- 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
- 15. JavaScript testing #15. Interpreting the code coverage metric
- 16. JavaScript testing #16. Snapshot testing with React, Jest, and Vitest
- 17. JavaScript testing #17. Introduction to End-to-End testing with Playwright
- 18. JavaScript testing #18. E2E Playwright tests for uploading and downloading files
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.
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.
There is also a shorter way to achieve the above by providing a function to the jest.fn() function.
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.
We can simplify the above by using the mockReturnValue function instead.
Returning a resolved promise
We often create mock functions that are supposed to return a promise that resolves to a particular value.
We can make the above more readable by using the mockResolvedValue function.
Returning a rejected promise
Also, we sometimes want our mock function to return a rejected promise.
We can make the above example simpler by using the mockRejectedValue function.
Spying on a mock function
To check if a function has been called, we can use the toBeCalled function.
If we want to be more precise, we can use the toBeCalledTimes and toBeCalledWith functions.
If we don’t care about one of the arguments of our function, we can use toBeCalledWith together with expect.anything().
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.
Through the mock.results property, we can get the results of all of the calls made to our mock function.
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.
A real-life example of spying
To better grasp the idea of spying, let’s create a straightforward set of functions.
parsePeople.ts
The above function takes an array of people and performs various operations on the data.
person.ts
One of the operations performed by parsePeople is figuring out the youngest person.
getYoungestPerson.ts
The other operation is grouping the people by country.
groupPeopleByCountry.ts
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
Let’s also write an elementary unit test for the getYoungestPerson function.
getYoungestPerson.test.ts
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
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
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
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
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.
If we want to reset only one mock, we can use the mockReset function.
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.