Decision Record: Mocking in Jest tests
This is a lightly edited copy of an Architecture Decision Record I wrote at work and share here in public.
Date: 2023-09-06
Background
We use a lot of mocking in tests written with Jest whenever modules need to leave the JavaScript system barrier. That is, whenever we make network requests, access the location, microphone or camera or some other device interaction, we have to mock those interactions. The rest of this document will call these “system interactions”.
The approach to mocking system interactions currently varies a lot, since we have no convention or agreement around these.
Reasons for Decision
To make Jest tests easier to write and read and increase consistency, we should decide on guidelines for mocking system interactions.
Options
Option 1: Focus on unit testing
Whenever a module (a React component, some other functions) depends on other modules that have system interactions, we mock those direct dependencies, so that the module under test only depends on pure functions or mocks. We mock as closely as possible to the tested module. That could mean jest.mock('@tanstack/react-query')
(when testing a custom hook using react-query) or jest.mock('../hooks/useGetUserData')
(when testing a component that uses a custom hook).
Since we mock a lot of our own code, we’ll use Manual Mocks correctly to reuse mocks and avoid duplicating mocks many times. Relates to ‘Bonus option 2’ below.
This doesn’t mean that we would mock all dependencies of a module – that would lead to an excessive amount of mocking with little additional value. The focus is on mocking those dependencies that directly or indirectly involved system interactions.
- Pros:
- Tests focus on one module and don’t break when dependencies change
- Cons:
- Need to write a test for each module, since there is no indirect coverage
- We’d write a lot of mocks, even for our own code, which could hide actual bugs if the mocks don’t reflect the actual module correctly
Option 2: Focus on integration testing
Whenever a module (a React component, some other functions) depends on other modules that have system interactions we mock the system APIs, like using nock
to mock the network requests or mocking react-native-permissions
. If a component uses a custom hook that uses react-query
to fetch some data, we’d mock the request for “fetch some data”, not the hook or react-query.
Tests without any system interactions won’t need any mocking, for example a pure function can be covered with a neat unit test without mocks.
- Pros:
- Increased test coverage of our own code – a single test can cover more than just the module under test
- Since we only mock system interactions on the “outside”, we can reuse a lot of mocks and don’t need to write them so often. See ‘Bonus option 2’ below for more details.
- Mocking network requests is more precise than mocking react-query or custom hooks – tests get closer to what the user does, with better runtime than e2etests
- Cons:
- Tests can break (false negatives) when imported modules change
Option 3: Combine 1 + 2, label clearly
We use both approaches of mocking outlined as option 1 and 2 above, but clearly label those tests.
// mocks direct dependencies if system interactions are involved
describe('useRecording unit tests', () => {
// mocks only system interactions, not direct dependencies
describe('AssignRecordingGroupModal integration tests', () => {
Within those categories, we apply the rules above, mocking system interactions as close as possible (unit tests) or as far away as possible (integrations), and don’t mix it up.
- Pros:
- More flexibility in how we write tests, leaves this decision up to the author
- A bit more consistency over the status quo, since we at least introduce definitions of unit and integration tests and how they are allowed to deal with system interactions
- Cons:
- Need to decide between unit and integration test for each module with system interactions, and potentially defend that decision
- All the cons of both options
Bonus option 1: Limit jest API usage for mocking
We agree to always use jest.mock()
and to forbid usage of jest.spyOn
. This is mostly based on various problems we’ve found related to jest.spyOn
when trying to switch from babel
to swc
.
For creating mocks that need to mock a module, but only customize some of it, we try to use jest.createMockFromModule(moduleName)
Bonus option 2: Convention for local vs global mocks
If a mock for a library (module loaded from node_modules) is needed in more than two test files, we move it to __mocks__
(in the root folder). The file name must match the module name used for importing the module, like __mocks__/lodash.js
to mock lodash
. If named correctly, these mocks are automatically loaded! There’s more details about this in the Jest docs – note the exception about built-in nodejs modules.
Since those mocks are automatically loaded, we try to (incrementally) replace all mocks from jest.config.js
‘s setupFilesAfterEnv
property and also replace them from setup.jest.js
.
Results
Adopt Option 2 (as above) along with the bonus options.
This is the best option to increase test coverage and consistency while keeping the number of mocks low.
We can revisit this decision if it turns out that this focus on integration tests has more or larger drawbacks than estimated here. We could then switch to Option 1 or 3, or refine our approach with Option 2.
Sources
- Jest docs about Manual Mocks, important read whatever our decision here, since our current use of global mocks is quite messy
- Martin Fowler in 2007 about Mocks vs Stubs and “mockist” testing – overall seems to prefer Option 2, which he calls “classical TDD”, while Option 1 would be “mockist TDD”
- Stop mocking fetch by Kent C. Dodds from 2020. His prefered tool
msw
is web-based, butnock
seems to be our closest equivalent. Overall seems to argue for Option 2 – keep mocks as far down the stack as possible, so that own and library code is covered in tests. In The Merits of Mocking from 2018 he also argues to mock network calls and animations, but “Other than that, most of my UI tests are using the real production code.” - The Practical Test Pyramid by Ham Vocke from 2018, published by Martin Fowler. Also talks a bit about mocking, but without an obvious recommendation related to this decision. Included here since there’s not much to find about when/how to mock in general.