— JavaScript, Software Testing — 2 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.
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.js2import Cookie from "cookies-js";3
4Cookie.get("key");5// ...6Cookie.set("key");
1// code.spec.js2let mockGetCookie = jest.fn();3let mockSetCookie = jest.fn();4
5jest.mock("cookies-js", () => ({6 __esModule: true, // mock the exports7 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});
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 test2// will still execute and your code will use this runtimeConfig.json file3jest.mock("./runtimeConfig.json", () => ({ env: "QA1" }), { virtual: true });
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.js2const 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.js2
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 this9 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});
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});
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});
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.
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.