JS tests: mocking best practices
Tags: testing, javascript, mocks
Published: 2022-12-29 20:02:56

## TL;DR

You can mock imports in multiple ways, all of them valid, but it's usually best to use `jest.mock()` for "static" mocks and `jest.spyOn()` for mocks you need to change the implementation for specific test cases.
There's also some global configuration and test file structure which can help us with that and to make sure our tests are independent and consistent.



## The problem

Tests are usually the forgotten ones when it comes to best practices. They're getting a similar treatment as CSS architecture or HTML accessibility, forgotten until you need to care about them.

It's also a fact that JS testing (and specially in the Frontend) is quite recent yet, and I'm sure this will improve with time, but the reality is that most companies still don't add tests to their JS codebases, and if they do, they're usually a mess.

In JS you can usually achieve the same result in multiple ways, but that doesn't mean all of them are equal. That's why I believe best practices are even more important in our ecosystem.


## What about mocks?

Mocks are probably the hardest part to keep organised and clean in your test suite. But there are some decisions you can make to help you with that.

The first thing you need to know is what kind of test you are writing. I usually refer to [this article](https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications) by Kent C. Dodds, one of (if not the most) relevant actors in the Javascript testing community. Depending on the kind of test, the data you're mocking and how you're doing it should change.


## Some considerations

In this article I'm going to give some examples written with Jest, since it's still the most popular JS test runner out there, but note that if you're using Vitest (and you probably should) almost all the syntax is the same, so this applies too.


## Global configuration

Neither Jest nor Vitest clear, reset or restore your mocks by default after each test. This is very opinionated, but imho that shouldn't be the default.

You want your tests to be consistent and independent from each other, so a test should not rely on a previous test to pass or fail.

Let's see what means to `clear`, `reset` and `restore` a mock, based on Jest's [docs](https://jestjs.io/docs/mock-function-api#mockfnmockclear):

|                                                    | clear | reset | restore |
|----------------------------------------------------|:-----:|:-----:|:-------:|
| Clears mock calls, instances, contexts and results |   ✅   |   ✅   |    ✅    |
| Removes mocked return value or implementation      |   ❌   |   ✅   |    ✅    |
| Restores the original implementation               |   ❌   |   ❌   |    ✅    |


So not activating `clearMocks` in your global config means a mocked function will keep all of its calls from previous tests, so you could easily get to a false positive/negative when asserting a mocked function has been called or not, and a test could even pass when we run the whole test suite but fail if we run it alone. **We don't want that**, so `clearMocks` should be set to `true` in the global config.

In regards to `resetMocks` and `restoreMocks`, it's not so straight forward. If we really want a clean state before each test, we should enable `restoreMocks`, since that is the only one that restores the original implementation. The caveat is that if we mock something for one test, we usually need that to be mocked for all tests in that test suite, maybe just with different mock implementations/return values.

Even there are some exceptions, I recommend setting `restoreMocks` to `true`, because that's the only way you'll be sure each test is independent and that will allow you to achieve a more sustainable test suite. But that's a strong choice, so you'll need to adapt the way you organise and set your mocks to avoid code duplicity hell in each test.

Luckily, there's a way to have the best of both worlds. Jest has `beforeEach`, `afterEach`, `beforeAll` and `afterAll` methods that, in conjunction with a good test structure, it can make sure you have the desired mocks for every test. But we'll come back to this later.


## `jest.fn()` vs `jest.spyOn()` vs `jest.mock()`

As I said, there are lots of ways you can mock things in JS with Jest or Vitest, but even though you can achieve a similar result with most of them it doesn't mean you should use them indistinctly.

Again, as per Jest [docs](https://jestjs.io/docs/jest-object):
* `jest.fn(implementation?)`: returns a new, unused mock function. Optionally takes a mock implementation.
* `jest.spyOn(object, methodName)`: creates a mock function similar to `jest.fn` but also tracks calls to `object[methodName]`. Returns a Jest mock function.
* `jest.mock(moduleName, factory, options)`: mocks a module with an auto-mocked version when it is being required. `factory` and `options` are optional.

### When to use each one?

I won't argue what should or shouldn't be mocked in this article, that's a whole different topic. Usually we'll be using this utilities to replace the behaviour of a function because we don't want it to affect the test outcome. That's useful when testing code that relies on other utilities, third party libraries or Backend endpoints.

#### `jest.fn`
This is the one we'll use most frequently. We'll usually use it to mock function parameters (or function props if we're testing components).
```javascript
const mockedFn = jest.fn(() => 'mocked return value');
const result = methodToTest(mockedFn)
```

#### `jest.spyOn`
We should use this one when we want to mock an imported method and we want different mocked implementations/return values depending on the test. For example, you want to test both the success and error flow after an endpoint call response, so your mock should resolve and throw respectively.
```javascript
const mockedFn = jest
  .spyOn(methodModule, 'methodName')
  .mockReturnValue('mocked return value');
const result = methodToTest()
```

#### `jest.mock`
When you want to mock a full imported module or some parts and you want it to have the same behaviour across all tests in that file, you'll use `jest.mock`. It's recommended to include `jest.requireActual` in the mock since that'll be leaving the rest of the module that you're not explicitly mocking with its original implementation.
```javascript
jest.mock('methodModule', () => ({
  ...jest.requireActual('methodModule'),
  methodName: jest.fn(() => 'mocked return value'),
})
```

### But why?
You could definitely use `jest.spyOn` and `jest.mock` both for consistent and changing implementations respectively, but I believe that this way makes more sense, and you'll get it when we get to the last section of this article.
The main goal is to achieve a well organised test structure, and we need to make some decisions to get there.
If you want to change the implementation/return value of an imported function mocked with `jest.mock`, you should previously declare a variable, assign it a `jest.fn` with the "default" implementation and then pass it to `jest.mock`. After that, you'll need to refer to that variable in the specific test you want to change its implementation, so it makes it a bit more verbose and makes your top-level mocks readability a bit more complex.
Besides, `jest.spyOn` allows you to only mock a specific element of the exported module without having to worry about overwriting the rest of the module exported elements.

## Test structure

You can think this is not relevant, I though it too not so long ago, but this can help you wrap everything mentioned before and make your tests more readable, sustainable and consistent. It's like the cherry on top where everything makes sense.

If you've been writing tests you'll already know we have `describe` and `it` blocks, the first one is used to group tests and the second one to define a specific test case.

We'll try to use the `describe` blocks to structure our tests taking into consideration the different scenarios where we need to test our code. That means we'll be using them to set mock implementations that will be shared across all test cases inside that block, and we can use the previously mentioned `beforeEach` method to achieve that.

### Show me the code

Let me write an example. Imagine we have a the following function:
```javascript
import { irrelevantMethod } from 'module-1';
import { getSomeThings } from 'module-2';

export function getArrayOfThings(amount) {
  if (!amount) {
    return [];
  }
  irrelevantMethod();
  const result = getSomeThings();
  if (!result) return [];
  return result.slice(0, Math.min(amount, 4));
}
```

Our function has some dependencies, which are:
* `irrelevantMethod`: this is a method our function has to call but that doesn't affect the outcome in any way (hence its name). A real-life example of this could be event tracking.
* `getSomeThings`: this method does affect the result of our function, so we'll be mocking it and changing its mocked return value in some of our tests. We're assuming we know this method can only return null or a valid array of a fixed considerable length.

If we put everything we've seen together, the test file for this method could look something like this:
```javascript
import * as module2 from 'module-2';
import { getArrayOfThings } from '../utils.js';

const mockedIrrelevantMethod = jest.fn();
jest.mock(() => ({
  ...jest.requireActual('module-1'),
  irrelevantMethod: mockedIrrelevantMethod,
}));

describe('getArrayOfThings', () => {
  it('should return an empty array if amount is 0', () => {
    const result = getArrayOfThings(0);
    expect(result).toEqual([]);
  });

  it('should call irrelevantMethod and getSomeThings if amount is greater than 0', () => {
    const mockedGetSomeThings = jest.spyOn(module2, 'getSomeThings');
    getArrayOfThings(1);
    expect(mockedIrrelevantMethod).toBeCalled();
    expect(mockedGetSomeThings).toBeCalled();
  });
  
  describe('getSomeThings returns null', () => {
    let mockedGetSomeThings;
    beforeEach(() => {
      mockedGetSomeThings = jest.spyOn(module2, 'getSomeThings').mockReturnValue(null);
    });
    
    it('should return an empty array', () => {
      const result = getArrayOfThings(1);
      expect(result).toEqual([]);
    });
  });
  
  describe('getSomeThings returns an array', () => {
    let mockedGetSomeThings;
    beforeEach(() => {
      mockedGetSomeThings = jest.spyOn(module2, 'getSomeThings').mockReturnValue([1, 2, 3, 4, 5, 6]);
    });
    
    it('should return an array of "amount" elements if "amount" is 4 or less', () => {
      const result = getArrayOfThings(3);
      expect(result).toEqual([1, 2, 3]);
    });
    
    it('should return an array of 4 elements if "amount" is greater than 4', () => {
      const result = getArrayOfThings(5);
      expect(result).toEqual([1, 2, 3, 4]);
    });
  });
});
```

This is a very simple example, sometimes you'll need to have multiple `describe` levels, each one with its `beforeEach` callback that will affect all the tests in that block. That's fine, and will actually help you keep your tests even more readable.

## Conclusion

This is a very opinionated way of organising tests and mocks, but after many years testing javascript code, it's by far the approach that works best for me.