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

Street coffee in Bulgaria

In Germany it’s very common for people to buy coffee in bakeries, where the person behind the counter will put the paper cup in a “fully” automated coffee machine and press the right button, then hand it out in exchange for money. In terms of potential automation and self-service, it’s a silly process.

In contrast, in Varna in Bulgaria I’ve seen many street coffee vending machines. Sometimes they feature brands like Lavazza, but mostly they’re unbranded and give you different types or mixes of coffee for very little money. Like a double espresso of reasonable taste, surely no worse than the machines in German bakeries, for 60 stotinki – 30 Euro cents!

Coffee vending machine with it’s owner

This one is maintained by Stefan (Стефан), from whom my father in law bought some kilograms of coffee for the mountain vacation. He operates 70 of these around Varna. I didn’t see any other of his, probably because there’s hundreds of them all around the city.

I didn’t learn where Stefan gets his coffee, but he’s got about 16 tons of it stored in the room behind the machine above.

Two tons of coffee
Parts of the coffee machines

A few days later we were in Vinarsko, a little village south of Aytos, around Burgas. There was no coffee in the house, so the need for coffee had to get satisfied by the vending machine in the village centre, located in front of the only store.

The coffee machine next to the store entrance (closed on Sunday)

This machine has so many stories to tell! Look at all the scratches:

The machine in all it’s glory

I guess an important part of coin-fed vending machines is “reading” the coins. Part of that experience is inserting the correct amount, but the machine then rejects it immediately. Another attempt might be accepted, or not. A countermeasure, whatever the mechanics are, seems to be to rub the side of the coin against the metal of the machine – as can be seen here, that has been done excessively!

The main panel

Note the new metal plate on the right. It says търкай тук, “rub here”. I’m not sure if it’s really new, or has been there and ignored for a long time.

When coins get rejected or change gets returned, you get to pick them up a little below – if you dare touch this little hole from hell:

The coin return, half burned away

Since these machines sell something, they also need to pay taxes. For this machine, that’s the most recent looking part. I didn’t understand if it only keeps track of purchases or actually reports them remotely immediately. Given Bulgaria has very good mobile network coverage, I wouldn’t be surprised if this contains a mobile connection:

I don’t know what the 2nd row means, but it seems tax related

That’s all for now about Bulgarian street coffee.

Next up on this trip is Chepelare, in the Rhodope mountains. I’ll keep an eye out for more of these machines.

New music in 2021

Inspired by Martin Fowler’s My favorite musical discoveries of 2021, I wanted to list my discoveries, too. Since I’ve only discovered 4 new bands in 2021, I’m also listing a few other albums I bought.

Links go to Bandcamp (allows you to listen to the full album before buying) and Amazon (has only ~30s samples).

Meat Mallet by A Formal Horse

This was a new discovery! A Formal Horse is prog rock with weird/strange/funny vocals, that stand out quite a lot. Like “I’m a lasagne, you’re the big cheese”. Or how in “You’ve got a billion and I’ve got a half” near the end “billion” gets repeated a lot, until it gradually turns into “oblivion”.

Cavalcade by black midi

My friend Marcus recommended Black Midi quite a lot – since 2019! I didn’t get hooked until last year, when I listened to this podcast episode with a conversation between Morgan Simpson (drummer in Black Midi) and Bill Bruford (King Crimson, Yes, among many other). Hearing them talk about the similarities between Black Midi and King Crimson got me interested and beyond the hurdle of “John L” (not an easy opener).

Etemen Ænka by DVNE

A new release from a prog/post metal band I already knew. Good stuff.

Summerland by Dool
Here Now, There Then by Dool

A new discovery! They tag it as “dark rock” on Bandcamp, I’ve also seen it as “doom rock”. I’ve listened to both albums a lot. I remember watching some live show with this intermission of them playing live at Wacken: “We’re hot, and horny. [pause] And you’re sexy! [mostly awkward silence]”.

Dodge and Burn by The Dead Weather

This was already released in 2015, but I missed it at the time. One of the bands I discovered while playing Guitar Hero. One of many bands Jack White is involved in (Wikipedia lists eight associated acts).

Aphelion by Leprous

Since seeing Leprous live late in 2019 (phew, lucky!) they moved from ‘band I like’ to ‘one of the best bands I know’. This new release didn’t blow me away, but I’m still hoping to see them live again sometime soon.

Moving Backwards by Wheel
Resident Human by Wheel

The final new discovery of 2021. A direct recommendation from Marcus for me. Worked really well, would buy again.

App idea: Game of QR

Imagine this: You’re outside, standing in line of your favorite bakery, and spot a QR code in the window of the store. You don’t care about the code itself, but pull out your phone anyway, open the Game of QR app and point your phone’s camera at the QR code. On the screen, it switches from the camera view to a 2D/flat version of the code, which starts animating using the rules of Conway’s Game of Life

  1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
  2. Any live cell with two or three live neighbours lives on to the next generation.
  3. Any live cell with more than three live neighbours dies, as if by overpopulation.
  4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

If you’re not familiar with this zero-player game, here’s a sample from Wikipedia:

By Lucas Vieira – Own work, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=101736

And a screenshot of my own implementation from 2020, which I called “Game of Ghosts”:

Two still lifes, two oscillators, one glider (bottom right)

Now imagine the AR (augmented reality) version: Still pointing the camera at the QR code, but now the animation happens in place, with the same perspective, instead of a reproduced 2D copy.

To make it more interesting, QR codes and their Game of Life states are collected in a gallery with geolocation. That way users of the app can collect QR codes, sharing the ones with interesting lifeforms.

Technical considerations

Wikipedia has a nice overview of QR codes, including all 7 variants. I had no idea so many exist – some even have color.

I’m still interested in learning more about React Native, so that’s where I’d start. The camera access should be possible with Expo, which would allow for a very low barrier to get started with development and testing. That also makes it easy to do beta testing, since there’s no need to upload and install apk files.

Implementing Game of Life itself is rather easy, which is probably why its a popular programming exercise. My CLI version implemented with node.js runs on replit.com.

I expect the difficult part to implement for Game of QR is to read the raw QR code as a 2D data structure, without decoding it. I assume QR code scanner libraries will include the decoding. At least the documentation for https://github.com/moaazsidat/react-native-qrcode-scanner doesn’t show how to access the raw data. Maybe the decoded data can be turned into a new QR code, which is then used to seed the Game of Life.

Let me know if you’re interested in collaborating with me on this idea!