Using rewire with Jest in Typescript

Want to test some private methods or mock unexposed functions? Having trouble using rewire in Typescript Land? You are in the right place.

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.

monument with statuettes on top building at daytime
The jury is still out trying to figure out wether or not testing private or internal methods should be done at all- Photo by Nils / Unsplash

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.

gray water plane toy
Rewire, saving the day - Photo by John Ruddock / Unsplash