Skip to content
goodguydaniel.com

Unrevealed tips for unit testing with Jest

JavaScript, Software Testing2 min read

As a front-end engineer by profession, and having a few JavaScript open source projects myself, I've faced some challenges around unit testing, in my particular case, with Jest.

For several times I question myself: "How did the test get to the point in which it's so much more complex to implement than the feature itself?" - It's usually like that, don't take me wrong here, I'm just referring to extreme cases, where 3rd party libraries or even architectural decisions, make your code super hard to test.

Nevertheless, I wanted to share with you a super pragmatic and short list of tips that might help you get that tricky stubborn mock to work.

1. Mocking defaults

In this example, I mock a 3rd party dependency called ScottHamper/Cookies. This 3rd party exports only a default property that is a wrapper to all the cookie operations that one can perform. Internally it's actually implemented as a singleton. The trick is to use __esModule: true flag within the jest.mock call.

1// code.js
2import Cookie from "cookies-js";
3
4Cookie.get("key");
5// ...
6Cookie.set("key");
1// code.spec.js
2let mockGetCookie = jest.fn();
3let mockSetCookie = jest.fn();
4
5jest.mock("cookies-js", () => ({
6 __esModule: true, // mock the exports
7 default: {
8 set: jest.fn().mockImplementation((...args) => {
9 mockSetCookie(...args);
10 }),
11 get: jest.fn().mockImplementation((...args) => {
12 mockGetCookie(...args);
13 }),
14 },
15}));
16// ...
17it("should call Cookie.set", () => {
18 expect(mockSetCookie).toHaveBeenCalledWith("key");
19});

2. Mocking files that don't exist in the file system! 🤯

If you depend on a file that is generated at runtime, or a file that only exists in the context of the client (e.g., a runtime configuration file), this file it's not most likely in your filesystem when you trigger the test, but your program depends on it to run. There's a way to mock this nonexistent file with Jest, one just needs to make use of the mock configuration flag virtual.

1// runtimeConfig.json does not exist on the filesystem, but your test
2// will still execute and your code will use this runtimeConfig.json file
3jest.mock("./runtimeConfig.json", () => ({ env: "QA1" }), { virtual: true });

3. Complex/large assertions on function calls

Consider the following scenario where you have a set of star wars characters and you can fetch them according to different kinds of sorting options.

1// code.js
2const starWarsCharacters = [
3 { name: "Luke Skywalker", height: "172", mass: "77" },
4 { name: "C-3PO", height: "167", mass: "75" },
5 { name: "R2-D2", height: "96", mass: "32" },
6 { name: "Darth Vader", height: "202", mass: "136" },
7 { name: "Leia Organa", height: "150", mass: "49" },
8 { name: "Owen Lars", height: "178", mass: "120" },
9 { name: "Beru Whitesun lars", height: "165", mass: "75" },
10 { name: "R5-D4", height: "97", mass: "32" },
11 { name: "Biggs Darklighter", height: "183", mass: "84" },
12 { name: "Obi-Wan Kenobi", height: "182", mass: "77" },
13];
14
15function getCharacters(options) {
16 if (options.sortedByName) {
17 return sortedByName(starWarsCharacters);
18 }
19
20 if (options.sortedByHeight) {
21 return sortByHeight(starWarsCharacters);
22 }
23
24 return starWarsCharacters;
25}

(Note! let's ignore the return value, let's say we were only interested in knowing whether or not sortedByName function was called).

You might have seen the following testing code somewhere:

1// code.spec.js
2
3it("should call sortedByName with all the star wars characters", () => {
4 const options = { sortedByName: true };
5
6 getCharacters(options);
7
8 // I'm exaggerating to prove a point, hope nobody actually does this
9 expect(mockSortedByName).toHaveBeenCalledWith([
10 { name: "Luke Skywalker", height: "172", mass: "77" },
11 { name: "C-3PO", height: "167", mass: "75" },
12 { name: "R2-D2", height: "96", mass: "32" },
13 { name: "Darth Vader", height: "202", mass: "136" },
14 { name: "Leia Organa", height: "150", mass: "49" },
15 { name: "Owen Lars", height: "178", mass: "120" },
16 { name: "Beru Whitesun lars", height: "165", mass: "75" },
17 { name: "R5-D4", height: "97", mass: "32" },
18 { name: "Biggs Darklighter", height: "183", mass: "84" },
19 { name: "Obi-Wan Kenobi", height: "182", mass: "77" },
20 ]);
21});

morpheus alternative meme

source: https://imgflip.com/memegenerator/31952703/morpheus

In this cases, you can make use of mockFn.mock.calls to assert on the arguments that were passed into the function call, and you can use snapshot testing in order to keep your test case gracefully thin.

1it("should call sortedByName with all the star wars characters", () => {
2 const options = { sortedByName: true };
3
4 getCharacters(options);
5
6 expect(mockSortedByName.mock.calls[0]).toMatchSnapshot();
7});

4. Inline require to mock large datasets

In the event of facing the ideal scenario where you need to mock a full JSON API response, you might want to place your JSON separate file and then use an inline require statement to mimic a response for a particular method.

Let's again make use of the previous example with data from the SWAPI (The Star Wars API).

If you would have a static list of all characters in you program, or a mock of a response that you would want to use within your test case, you case use an inline require statement (in alternative of importing the mock on the top of the file).

1it("should get sort characters", () => {
2 const result = sortedByName(require("./starWarsCharacters.json"));
3
4 expect(result).toMatchSnapshot();
5});

5. Make use of async/await

It might also come the time where you need to perform some asynchronous work inside your test case, in that case, don't hesitate in recurring to async/await.

1it("get all star wars characters", async () => {
2 const data = await fetchStarWarsCharacters();
3
4 expect(data).toMatchSnapshot();
5});

Oh, and there's also .resolves.

1it("get all star wars characters (with resolves)", () => {
2 const promise = fetchStarWarsCharacters();
3
4 return expect(promise).resolves.toMatchSnapshot();
5});

Do you have more tips that in your opinion, fit nicely in this list? I would love to hear them.

Updates

When using babel-jest, calls to mock will automatically be hoisted to the top of the code block. Use this method if you want to explicitly avoid this behavior.

Think of jest.doMock as a resource for you to mock differently the same module in various test cases that live under the same test file.

Cheers!

If you liked this article, consider sharing (tweeting) it to your followers.