- 1. JavaScript testing #1. Explaining types of tests. Basics of unit testing with Jest
- 2. JavaScript testing #2. Introducing Enzyme and testing React components
- 3. JavaScript testing #3. Testing props, the mount function and snapshot tests.
- 4. JavaScript testing #4. Mocking API calls and simulating React components interactions
- 5. JavaScript testing #5. Testing hooks with react-hooks-testing-library and Redux
- 6. JavaScript testing #6. Introduction to End-to-End testing with Cypress
- 7. JavaScript testing #7. Diving deeper into commands and selectors in Cypress
- 8. JavaScript testing #8. Integrating Cypress with Cucumber and Gherkin
- 9. JavaScript testing #9. Replacing Enzyme with React Testing Library
- 10. JavaScript testing #10. Advanced mocking with Jest and React Testing Library
- 11. JavaScript testing #11. Spying on functions. Pitfalls of not resetting Jest mocks
- 12. JavaScript testing #12. Testing downloaded files and file inputs with Cypress
- 13. JavaScript testing #13. Mocking a REST API with the Mock Service Worker
- 14. JavaScript testing #14. Mocking WebSockets using the mock-socket library
Jest offers a lot of functionalities for mocking functions and spying on them. Unfortunately, there are some pitfalls we need to watch out for. In this article, we explain how spying on functions works. We also explain how and why we should reset our mocks.
Creating mock functions
The most straightforward way of creating a mock function is to use the jest.fn() method.
1 |
const mockFunction = jest.fn(); |
A mock function has a set of useful utilities that can come in handy in our tests. One of them is the mockImplementation function that allows us to define the implementation of our function.
1 2 3 4 5 |
mockFunction.mockImplementation((numberOne: number, numberTwo: number) => { return numberOne + numberTwo; }); mockFunction(10, 5); // 15 |
There is also a shorter way to achieve the above by providing a function to the jest.fn() function.
1 2 3 |
const mockFunction = jest.fn((numberOne: number, numberTwo: number) => { return numberOne + numberTwo; }); |
Shorter variants of the mockImplementation function
Particular use-cases of the mockImplementation function can get pretty repetitive. Thankfully, Jest provides us with its shorter variants.
Returning a value
For example, we often want to create a mock function that returns a certain value.
1 2 3 4 5 6 7 8 |
const getUsersMock = jest.fn(); getUsersMock.mockImplementation(() => { return [ { name: 'John' } ] }); |
We can simplify the above by using the mockReturnValue function instead.
1 2 3 4 5 6 |
const getUsersMock = jest.fn(); getUsersMock.mockReturnValue([ { name: 'John' } ]); |
Returning a resolved promise
We often create mock functions that are supposed to return a promise that resolves to a particular value.
1 2 3 4 5 6 7 8 |
const fetchUsersMock = jest.fn(); fetchUsersMock.mockImplementation(() => { return Promise.resolve([ { name: 'John' } ]); }); |
We can make the above more readable by using the mockResolvedValue function.
1 2 3 4 5 6 |
const fetchUsersMock = jest.fn(); fetchUsersMock.mockResolvedValue([ { name: 'John' } ]); |
Returning a rejected promise
Also, we sometimes want our mock function to return a rejected promise.
1 2 3 4 |
const fetchUsersMock = jest.fn(); fetchUsersMock.mockImplementation(() => { return Promise.reject('Fetching users failed'); }); |
We can make the above example simpler by using the mockRejectedValue function.
1 2 |
const fetchUsersMock = jest.fn(); fetchUsersMock.mockRejectedValue('Fetching users failed'); |
Spying on a mock function
To check if a function has been called, we can use the toBeCalled function.
1 2 3 4 5 6 7 8 9 10 11 |
const mockFunction = jest.fn((numberOne: number, numberTwo: number) => { return numberOne + numberTwo; }); mockFunction(10, 5); describe('The mock function', () => { it('should be called', () => { expect(mockFunction).toBeCalled(); }); }); |
If we want to be more precise, we can use the toBeCalledTimes and toBeCalledWith functions.
1 2 3 4 5 6 7 8 |
mockFunction(10, 5); describe('The mock function', () => { it('should be called', () => { expect(mockFunction).toBeCalledTimes(1); expect(mockFunction).toBeCalledWith(10, 5) }); }); |
If we don’t care about one of the arguments of our function, we can use toBeCalledWith together with expect.anything().
1 2 3 4 5 6 7 |
mockFunction(10, 5); describe('The mock function', () => { it('should be called', () => { expect(mockFunction).toBeCalledWith(expect.anything(), 5) }); }); |
expect.anything() matches anything besides null and undefined.
Getting more details about our mock
We can also access the details of the calls to our function through the mock.calls property.
1 2 3 4 5 6 7 8 |
mockFunction(10, 5); mockFunction(20, 10); console.log(mockFunction.mock.calls); //[ // [10, 50], // [20, 10], //] |
Through the mock.results property, we can get the results of all of the calls made to our mock function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
mockFunction(10, 5); mockFunction(20, 10); console.log(mockFunction.mock.results); //[ // { // type: 'return', // value: 15 // }, // { // type: 'return', // value: 30 // } //] |
The type property would contain 'incomplete' if a call started, but didn’t complete yet. The type would contain 'throw' if the call would complete by throwing a value.
The mock.instances property contains all instances of objects created using our mock function and the new keyword.
1 2 3 4 5 6 7 |
const mockConstructor = jest.fn(); const firstObject = new mockConstructor(); const secondObject = new mockConstructor(); console.log(mockConstructor.mock.instances[0] === firstObject); // true console.log(mockConstructor.mock.instances[1] == secondObject); // true |
A real-life example of spying
To better grasp the idea of spying, let’s create a straightforward set of functions.
parsePeople.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import Person from './person'; import getYoungestPerson from './getYoungestPerson'; import groupPeopleByCountry from './groupPeopleByCountry'; function parsePeople(people?: Person[]) { if (!people) { return null; } const peopleGroupedByCountry = groupPeopleByCountry(people); const youngestPerson = getYoungestPerson(people); return { peopleGroupedByCountry, youngestPerson, }; } export default parsePeople; |
The above function takes an array of people and performs various operations on the data.
person.ts
1 2 3 4 5 6 7 |
interface Person { name: string; age: number; country: string; } export default Person; |
One of the operations performed by parsePeople is figuring out the youngest person.
getYoungestPerson.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import Person from './person'; function getYoungestPerson(people: Person[]) { return people.reduce<Person | null>((youngestPerson, currentPerson) => { if (!youngestPerson) { return currentPerson; } if (currentPerson.age < youngestPerson.age) { return currentPerson; } return youngestPerson; }, null); } export default getYoungestPerson; |
The other operation is grouping the people by country.
groupPeopleByCountry.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import Person from './person'; function groupPeopleByCountry(people: Person[]) { return people.reduce<Record<string, Person[]>>((result, person) => { const currentCountryData = [ ...(result[person.country] || []), person ]; return { ...result, [person.country]: currentCountryData, }; }, {}); } export default groupPeopleByCountry; |
Writing simple unit tests
Instead of writing unit tests for all of the above functions, we might want to write one integration test for the parsePeople function. In some cases, though, we might want to prefer to write unit tests for each function separately. So let’s start by writing a straightforward test for the groupPeopleByCountry function.
groupPeopleByCountry.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
import Person from './person'; import groupPeopleByCountry from './groupPeopleByCountry'; describe('The groupPeopleByCountry function', () => { describe('when provided with an array of people', () => { it('should group them by country', () => { const people: Person[] = [ { name: 'John', age: 30, country: 'USA', }, { name: 'Jane', age: 23, country: 'USA', }, { name: 'Adam', age: 31, country: 'Poland', }, ]; const result = groupPeopleByCountry(people); expect(result).toEqual({ USA: [ { name: 'John', age: 30, country: 'USA', }, { name: 'Jane', age: 23, country: 'USA', }, ], Poland: [ { name: 'Adam', age: 31, country: 'Poland', }, ], }); }); }); }); |
Let’s also write an elementary unit test for the getYoungestPerson function.
getYoungestPerson.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import Person from './person'; import getYoungestPerson from './getYoungestPerson'; describe('The getYoungestPerson function', () => { describe('when provided with an array of people', () => { it('should return the youngest person', () => { const people: Person[] = [ { name: 'John', age: 30, country: 'USA', }, { name: 'Jane', age: 23, country: 'USA', }, { name: 'Adam', age: 31, country: 'Poland', }, ]; const result = getYoungestPerson(people); expect(result).toEqual({ name: 'Jane', age: 23, country: 'USA', }); }); }); }); |
Mocking certain functions when writing unit tests
Thanks to the above tests, we’ve got a lot of the code covered already. We still need to test the parsePeople function. If we want to take a unit-testing approach, we can start by mocking groupPeopleByCountry and getYoungestPerson functions.
parsePeople.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import Person from './person'; import getYoungestPerson from './getYoungestPerson'; import groupPeopleByCountry from './groupPeopleByCountry'; jest.mock('./getYoungestPerson', () => ({ __esModule: true, default: jest.fn(), })); jest.mock('./groupPeopleByCountry', () => ({ __esModule: true, default: jest.fn(), })); describe('The parsePeople function', () => { let youngestPerson: Person; let peopleGroupedByCountry: Record<string, Person>; beforeEach(() => { youngestPerson = { name: 'Jane', age: 23, country: 'USA', }; peopleGroupedByCountry = {}; (getYoungestPerson as jest.Mock).mockReturnValue(youngestPerson); (groupPeopleByCountry as jest.Mock).mockReturnValue(peopleGroupedByCountry); }); // ... }); |
If you want to know more about mocking modules, check outJavaScript testing #10. Advanced mocking with Jest and React Testing Library
Thanks to the above, when we call the parsePeople function in our tests, Jest does not call the real groupPeopleByCountry and getYoungestPerson functions. Thanks to that, we can focus on testing the logic contained by the parsePeople function. That has a set of consequences:
- we can focus on writing tests that check only the parsePeople function,
- our parsePeople test does not care much about the implementation of the groupPeopleByCountry and getYoungestPerson functions,
- if either groupPeopleByCountry or getYoungestPerson function breaks, the unit test we wrote for groupPeopleByCountry will still be successful.
parsePeople.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
import parsePeople from './parsePeople'; import Person from './person'; import getYoungestPerson from './getYoungestPerson'; import groupPeopleByCountry from './groupPeopleByCountry'; jest.mock('./getYoungestPerson', () => ({ __esModule: true, default: jest.fn(), })); jest.mock('./groupPeopleByCountry', () => ({ __esModule: true, default: jest.fn(), })); describe('The parsePeople function', () => { let youngestPerson: Person; let peopleGroupedByCountry: Record<string, Person>; beforeEach(() => { youngestPerson = { name: 'Jane', age: 23, country: 'USA', }; peopleGroupedByCountry = {}; (getYoungestPerson as jest.Mock).mockReturnValue(youngestPerson); (groupPeopleByCountry as jest.Mock).mockReturnValue(peopleGroupedByCountry); }); describe('when provided with an array', () => { let peopleArray; beforeEach(() => { peopleArray = []; }); it('should call the getYoungestPerson function', () => { parsePeople(peopleArray); expect(getYoungestPerson).toBeCalledWith(peopleArray); }); it('should call the groupPeopleByCountry function', () => { parsePeople(peopleArray); expect(groupPeopleByCountry).toBeCalledWith(peopleArray); }); it('should return the return value of the getYoungestPerson function', () => { const result = parsePeople(peopleArray); expect(result?.youngestPerson).toEqual(youngestPerson); }); it('should return the return value of the groupPeopleByCountry function', () => { const result = parsePeople(peopleArray); expect(result?.peopleGroupedByCountry).toEqual(peopleGroupedByCountry); }); }); describe('when not provided with arguments', () => { it('should return null', () => { const result = parsePeople(); expect(result).toBe(null); }); it('should not call the getYoungestPerson function', () => { parsePeople(); expect(getYoungestPerson).not.toBeCalled(); }); it('should not call the groupPeopleByCountry function', () => { parsePeople(); expect(groupPeopleByCountry).not.toBeCalled(); }); }); }); |
The issue of sharing mocks between tests
Unfortunately, some of the above tests are failing.
Let’s look at the reason why those tests are not passing.
Error: expect(jest.fn()).not.toBeCalled()
Expected number of calls: 0
Received number of calls: 4
When we look at the implementation of the parsePeople function, we can see that if there are no arguments, the function returns null immediately. This is because Jest shares our mocks between tests in a particular test file by default.
parsePeople.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
describe('when provided with an array', () => { let peopleArray; beforeEach(() => { peopleArray = []; }); it('should call the getYoungestPerson function', () => { parsePeople(peopleArray); expect(getYoungestPerson).toBeCalledWith(peopleArray); }); }); describe('when not provided with arguments', () => { it('should not call the getYoungestPerson function', () => { parsePeople(); expect(getYoungestPerson).not.toBeCalled(); }); }); |
Above, in the first test, we call the
parsePeople with an array. Because of that, it calls the
getYoungestPerson mock function under the hood.
When we execute the second test right after the first one, the
getYoungestPerson mock is already called. Because of that,
expect(getYoungestPerson).not.toBeCalled() fails.
Clearing mocks
To deal with the above issue, we can force Jest to clear all the mocks after each test.
parsePeople.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
jest.mock('./getYoungestPerson', () => ({ __esModule: true, default: jest.fn(), })); jest.mock('./groupPeopleByCountry', () => ({ __esModule: true, default: jest.fn(), })); describe('The parsePeople function', () => { afterEach(() => { jest.clearAllMocks(); }) // ... }); |
When we run jest.clearAllMocks(), Jest clears the information stored in the mock.calls, mock.results, and mock.instances arrays of all mocks. Thanks to doing that after each test, the counter of calls made to our mock function contains zero. Because of that, our tests no longer fail.
If we want to clear just a particular mock, we can call mockFunction.mockClear();
If we want to clear the mocks after every test in all of our test files, we can add "clearMocks": true to our Jest configuration instead of running jest.clearAllMocks().
Resetting mocks
Besides the above, we also have the jest.resetAllMocks() function that is quite similar. Besides clearing the mock.calls, mock.results, and mock.instances properties, it also removes the implementation of our mock.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
jest.mock('./getYoungestPerson', () => ({ __esModule: true, default: jest.fn(), })); jest.mock('./groupPeopleByCountry', () => ({ __esModule: true, default: jest.fn(), })); describe('The parsePeople function', () => { afterEach(() => { jest.resetAllMocks(); }) // ... }); |
If we want to reset only one mock, we can use the mockReset function.
1 2 3 4 5 6 7 8 9 10 11 |
const mockFunction = jest.fn((numberOne: number, numberTwo: number) => { return numberOne + numberTwo; }); console.log(mockFunction(1, 2)); // 3 console.log(mockFunction.mock.calls.length); // 1 mockFunction.mockReset(); console.log(mockFunction.mock.calls.length); // 0 console.log(mockFunction(1, 2)); // undefined |
We might want to reset the mock after each of our tests in all of our test files. In this case, we need to set "clearMocks": true in our Jest configuration.
An important caveat is that Create React App sets clearMocks to true by default.
Summary
In this article, we’ve created mock functions and defined their implementation. We’ve also learned different ways of spying on a mock function. Finally, we wrote a unit test that uses all of the above knowledge to internalize this knowledge better. While doing that, we stumbled upon an issue of sharing mocks. Besides that, we learned how to clear the mocks between tests to prevent our tests from failing.