Using rewire with Jest in Typescript
Do you know rewire ? Long story short, it's awesome. Basically, it let's you get and set internal unexposed methods in js files. What for ? Well... unit testing.
For us, testing isn't only about making sure we don't break the public interface. It's also about debugging. And sometimes, some code is just complex and critical enough that you don't want to have to risk breaking the internals. Plus, well testing a few, small, independent functions is much easier than testing the public function putting them together.
That's where rewire comes in.
Access & test unexposed functions with rewire
Now, say we have, hum... a farm, with some animals. You could end up making (amongst many other things) a module similar to this:
const catSoundRegex = /(me+o+w+)/gi;
const cowSoundRegex = /(mo{2,})/gi;
const sheepSoundRegex = /(me{2,}h+)/gi;
const elfitzSoundRegex = /(\.{3})|(tap)|(wat\?!)/;
enum animals {
cat = "cat",
cow = "cow",
sheep = "sheep",
elfitz = "elfitz",
}
interface AnimalRecordingsDictionary {
[key: string]: string;
}
interface AnimalWithNoisiness {
name: animals;
noisiness: number;
}
export async function getNoisiestAnimalOnTheFarm(farmName: string, fetchFarmAnimalsWithSoundsRecording: (farmName: string) => Promise<AnimalRecordingsDictionary>): Promise<animals> {
try {
// 1. List all the farm's animals and get their sound recording
const farmAnimalsWithSounds = await fetchFarmAnimalsWithSoundsRecording(farmName);
// 2. Figure out each animal's noisiness, but for the purpose of this post I was lazy. Sorry.
const farmAnimalsWithNoisiness: AnimalWithNoisiness[] = Object.entries(farmAnimalsWithSounds).reduce((acc: AnimalWithNoisiness[], [name, sound]: string[]) => {
return acc.concat([ { name: name as animals, noisiness: figureOutAnimalNoisiness(name, sound)} ])
}, []);
// 3. Figure out the noisiest animal of them all and return it
return farmAnimalsWithNoisiness.reduce((max, current) => max.noisiness > current.noisiness ? max : current).name;
} catch (error) {
// tslint:disable-next-line:no-console
console.log("Error: ", error);
throw(error);
}
}
function figureOutAnimalNoisiness(animalName: string, soundString: string): number {
let regex;
switch (animalName) {
case animals.cat:
regex = catSoundRegex;
break
case animals.cow:
regex = cowSoundRegex;
break
case animals.sheep:
regex = sheepSoundRegex;
break
case animals.elfitz:
regex = elfitzSoundRegex;
break
default:
throw new Error("This isn't an animal !");
}
const regexResult = soundString.match(regex);
if (regexResult !== null) return regexResult.length;
throw new Error("The regex match failed. This isn't supposed to happen !");
}
Testing the export function, getNoisiestAnimalOnTheFarm
, is easy. Especially since most of it's dependencies are injected (see these answers on StackOverflow for more about dependency injections. They cite great resources on this matter) and can thus easily be mocked. But how about figureOutAnimalNoisiness
? How do you test it ? ( some people would say you don't.) How do you mock it ?
Testing an inacessible / unexposed method via rewire
So, for this you'll need jest, ts-jest, @types/jest (so your IDE doesn't complain), @types/rewire (same), rewire and, of course, typescript. All should go into your dev dependencies.
Here's how you'd get figureOutAnimalNoisiness
in order to test it
const exampleModule = rewire("./path/to/your/module/built/file");
const handleStepCompletion = exampleModule.__get__("figureOutAnimalNoisiness");
Pretty simple, right ? Now, there's a catch when using rewire with typescript. Notice how path in the rewire()
call is not your module's path ? Instead, it's the path to typescript's output for your module. Took us some time to figure that one out. That said, here's how you could test the function:
import rewire from "rewire";
import { animals, AnimalRecordingsDictionary } from "../src/exampleCode";
describe("Animals Noisiness Measurement Module Tests", () => {
const exampleModule = rewire("../lib/exampleCode");
const figureOutAnimalNoisiness = exampleModule.__get__("figureOutAnimalNoisiness");
const animalsRecordingsDictionary: AnimalRecordingsDictionary = {
"cat": "meowmeowmeeeeooooowmeowmeeoooooowwwwwmeow",
"cow": "moooooohhhhhh",
"sheep": "meeeeeeeehhhhhh",
"elfitz": "...taptaptaptaptaptaptaptaptaptaptaptaptap...what?!",
};
const results = Promise.all(Object.entries(animalsRecordingsDictionary).map((animal, string) => figureOutAnimalNoisiness(animal, string)));
const expectedResults = [
animalsRecordingsDictionary.cat.length,
animalsRecordingsDictionary.cow.length,
animalsRecordingsDictionary.sheep.length,
animalsRecordingsDictionary.elfitz.length,
];
it("should return a 4 items array", () => {
return expect(results).resolves.toHaveLength(4);
});
it("should return the expected results", () => {
return expect(results).resolves.toStrictEqual(expectedResults);
});
});
Just as easy ! Now, even though we don't mock unexposed methods in our team, let's get on with mocking !
Mocking an inacessible / unexposed method via rewire
Following rewire's documentation on Github, here's how you'd mock / set the function:
const exampleModule = rewire("./path/to/your/module/built/file");
const mock = (animalName: string, recording: string) => {
switch (animalName) {
}
};
exampleModule.__set__("figureOutAnimalNoisiness", mock);
And that's how you'd use it in a test !
import rewire from "rewire";
import { animals, AnimalRecordingsDictionary } from "./exampleCode";
describe("Exported function with mocked private function test", () => {
const exampleModule = rewire("../lib/exampleCode");
const mock = (animalName: string, recording: string) => {
switch (animalName) {
}
};
exampleModule.__set__("figureOutAnimalNoisiness", mock);
const animalsRecordingsDictionary: AnimalRecordingsDictionary = {
"cat": "meowmeowmeeeeooooowmeowmeeoooooowwwwwmeow",
"cow": "moooooohhhhhh",
"sheep": "meeeeeeeehhhhhh",
"elfitz": "...taptaptaptaptaptaptaptaptaptaptaptaptap...wat?!",
};
const fetchFarmAnimalsWithSoundsRecording = (farmName: string) => {
return Promise.resolve(animalsRecordingsDictionary);
};
const result = exampleModule.getNoisiestAnimalOnTheFarm("myFarmFromOuterSpace", fetchFarmAnimalsWithSoundsRecording);
it("should return", () => {
return expect(result).resolves.toBeDefined();
});
it("should return the expected result", () => {
return expect(result).resolves.toStrictEqual(animals.elfitz);
});
});
And... that's it !
If you want to have a look at the whole project, with the package.json, tsconfig, etc setup, check out the repo on Github !
Also, if you didn't know how to do it already, you've just seen how to test asynchronous code with jest ! Yes, it is that easy (more on this here), as long you stay away from aws-sdk-mock or mocking your module's imports using jest spies (more on this later).
Conclusion
As I wrote before, some people would say that, when it comes to tests, only exposed interfaces matter.
That everything else is just implementation details. That code that can't be directly tested from the interface shouldn't. That if something really does need to be tested, it should be exposed or separated into it's own class.
But there are times where the implementation is crucial, critical even, or just plain complex, handling a great variety of cases, and you both don't want anyone to use it directly (or even just be tempted to do so), don't want to break it inadvertently, and don't want to spend hours debugging should it ever break in production.
That's where rewire comes in.