In the previous tutorial, we discussed how to take your codemod skills to the next level by applying the techniques used by professionals to write a codemod like no-vars
.
In this tutorial, we take it one step further by writing test cases that confirm that our codemods identify all essential code patterns and then perform transforms correctly and precisely. Writing tests for our codemods can significantly improve your codemod development process.
Keep reading to add this skill to your arsenal.
Prerequisites
- Understanding ASTs - This article requires a good understanding of how to inspect, traverse, and manipulate ASTs.
- Advanced Understanding of Codemods - This article requires an advanced understanding of writing codemods. Before jumping into this tutorial, make sure you've gone through our tutorial on writing codemods like a pro.
Why Write Test Cases for Your Codemods?
Writing test cases for codemods is an extremely valuable practice.
Test cases help with:
- Ensuring that the codemod is working correctly as it’s being developed.
- Having a guideline of the requirements your codemod should satisfy as you develop it.
- Catching codemod regressions as the codemod evolves in the future.
- Documenting how the codemod is supposed to work for people in your team or community contributors.
- Ensuring that the codemod works across different environments.
With that being said, let’s learn by example how to write our first codemod test case.
Scenario
In our previous tutorial, we created a set of code patterns we keep track of to ensure that our codemod conforms to its requirements while it’s being built.
Our identified code patterns for the no-vars
codemod included some patterns that should be detected and others that should not be detected. We categorized the identified code patterns into:
- Patterns where we transform
var
declarations intolet
. - Patterns where we transform
var
declarations intoconst
. - Patterns where we keep
var
declarations unchanged (should not be detected).
Our goal now is to use the code patterns’ before and after snippets as a basis for our test cases.
Now let’s turn those code patterns into test cases!
Writing Our First Codemod Test Case
Writing tests for our codemods is actually a very simple process.
Our workflow for writing the test cases will be as follows:
- Preparing our testing environment.
- Creating our transform and test files.
- Writing our test cases.
- Running our test cases.
#1 Preparing Our Testing Environment
Usually, when you’re preparing your testing environment, you would need to:
- Install a testing library (ex: Mocha)
- Install an assertion library (ex: Chai)
- Creating a testing directory
- Adding test files for your testing directory
Alternatively, you could use our codemod registry, which already has the required libraries configured as well as a neat directory configuration for testing your codemods while eloquently integrating into the codemod platform.
We’ll continue this tutorial using the codemod registry, as it’s a simpler and more scalable approach for codemod builders.
All you need to do is:
Clone Intuita’s codemod registry on your local environment.
git clone https://github.com/intuita-inc/codemod-registry
Open the codemod registry directory in your code editor of choice.
#2 Creating Our Test Files
Test files contain all the code patterns that should be detected by the codemod for transformation, as well as tricky code patterns that should not be detected or transformed. The goal of the test file is to define all the patterns that, collectively, reduce the number of false positives and false negatives in our codemod.
Now that we have our local environment set up, we can begin by creating our test files. To do so, we will:
Navigate to the codemods/jscodeshift directory.
Run
npm install
.Create a new folder for your codemod. In this example, we’ll call it
no-vars
. If you plan to use the codemod registry for community contribution, please refer to our article about adding codemods to the codemod registry, where we go over the best practices to add your codemod to the registry.In our new directory, we’ll add an
index.js
file and atest.ts
file. The former will include our codemod code, while the latter will include our test cases.Add our codemod to
index.js
. We will be using our previously written codemodno-vars
, feel free to apply this process to a codemod you’ve written as well. At this stage, theindex.js
file will look as shown below:
export default function transformer(file, api, options) {
const j = api.jscodeshift;
const root = j(file.source);
.
..
...
//Lots of helper functions
...
..
.
const updatedAnything = root
.find(j.VariableDeclaration)
.filter(dec => dec.value.kind === 'var')
.filter(declaration => {
return declaration.value.declarations.every(declarator => {
return !isTruelyVar(declaration, declarator);
});
})
.forEach(declaration => {
const forLoopWithoutInit = isForLoopDeclarationWithoutInit(declaration);
if (
declaration.value.declarations.some(declarator => {
return (!declarator.init && !forLoopWithoutInit) || isMutated(declaration, declarator);
})
) {
declaration.value.kind = 'let';
} else {
declaration.value.kind = 'const';
}
})
.size() !== 0;
return updatedAnything ? root.toSource() : null;
}
#3 Writing Our Test Cases
At this stage, we should write test cases that check for code patterns that should be detected, as well as those that should not be detected.
Let’s start by writing our test cases in the test.ts
file.
We’ll start first by adding a describe()
function, which, you guessed it, describes what our test file (as a whole) does. In this case, we can just use 'no-vars'
as a simple description. If you’re writing test cases for migrating a codebase from one framework version to another, it would be a good idea to name the description something that describes the framework version and codemod name (ex: 'next 13 an-amazing-codemod'
).
Our file will now look like so:
import { FileInfo } from 'jscodeshift';
import assert from 'node:assert';
import transform from '.';
describe('no-vars', function () {
//Will add test cases here
});
Now, all we have to do is add test cases for specific code patterns we identified inside our describe()
function. We add test cases using it()
. The it()
function accepts 2 parameters:
- A test case description
- A function that specifies the test case
Let’s say for example we want to write a test case that checks if the transform we wrote changed the var
variable declarator into let
in case of the variable being mutated, we would end up with something like:
import { FileInfo } from 'jscodeshift';
import assert from 'node:assert';
import transform from '.';
describe('no-vars', function () {
it('should convert var to let if var is mutated', function () {
//Write test case details here
});
});
Now, all we have to do is write the expected input and output then check if our transform correctly converts from our input state to the other.
To do this, we should:
- Specify an input string that contains the ‘before’ state of the code pattern.
- Specify an output string that contains the ‘after’ state of the code pattern.
- Specify the associated JSCodeshift codemod (
index.js
in our case). - Get the actual output when we run the transform on our input code pattern.
- Check whether the actual output matches the specified expected output.
By applying those steps, we should end up with our test case as illustrated below:
import { FileInfo } from 'jscodeshift';
import assert from 'node:assert';
import transform from '.';
describe('no-vars', function () {
it('should convert var to let if var is mutated', function () {
//How the code should look like before applying our transform
const INPUT = `
var x = 1;
x = 2;
`;
const OUTPUT = `
let x = 1;
x = 2;
`;
//Our target transform and target input. In this case, we are using index.js and the INPUT const
const fileInfo: FileInfo = {
path: 'index.js',
source: INPUT,
};
//The actual output after running our target transform on the input source
const actualOutput = transform(fileInfo, this.buildApi('jsx'), {});
//Checking if actualOutput matches the specified OUTPUT const
assert.deepEqual(
actualOutput?.replace(/\W/gm, ''),
OUTPUT.replace(/\W/gm, ''),
);
});
});
From this point forward, you can specify as many it()
functions as you want which cover all the code patterns you’ve identified.
You can use advanced codemod platforms such as Codemod Studio to get a list of test cases or skeletons for your Mocha test file to boost your test development process!
Using Codemod Studio can instantly give you a list of test cases such as (from our previous tutorial “Write Codemods Like A Pro”):
- Test that the variable
notMutatedVar
is declared as aconst
variable and not mutated. - Test that the variable
mutatedVar
is declared as alet
variable and mutated inside thefor
loop. - Test that the variable
anotherInsideLoopVar
is declared as aconst
variable inside thefor
loop and not mutated. - Test that the
for
loop within
keyword transforms the variable declaration ofx
to aconst
variable declaration. - Test that the
for
loop within
keyword does not modify the behavior of the loop and produces the same output as before the transformation.
which you can also export as a skeleton Mocha test file!
#4 Running Our Test Cases
Finally, to run our test cases we should:
- Return to the codemods/jscodeshift directory.
- Run
npm test
Mocha should now do its magic and run all our test cases, providing a report of test statuses as shown below.
In the case that our codemod doesn’t work as it should, our test case will now easily catch the issue and report it so you can correct any inaccuracies in your codemod.
Takeaways
- Use your codemod patterns as a basis to write your test cases by converting code patterns into test cases using the before and after code states.
- Regularly run your test cases during codemod development to ensure that your codemods satisfy the end results you expect to see in your codebase.
- Use your test cases as your main reference during development and update your test cases if any new edge cases arise. This allows you to confidently rely on your test results during development.
- Break up code patterns over multiple
it()
test cases. This allows others to easily understand and improve on your codemods in the future. - Always use understandable test case descriptions and include comments that describe what specific test cases do.