Mocking with Jest: Taking Advantage of the Module System

Matija Marohnić

Jest has lots of mocking features. For a long time I’ve been using only a small subset of them, but with experience I was able to gain a deeper understanding of these features. Now I want to share that knowledge with you because it has been incredibly useful to me.

This can be an intimidating area for beginners, especially because at the time of this writing the Jest documentation on this subject is a bit spotty. Add to that the fact that the term “mock” is ambiguous; it can refer to functions, modules, servers etc. I would like to help you get familiar not only with mocking features in Jest, but these testing concepts in general.

While this blog posts reads fine on its own, some of the references are from Mocking with Jest: Spying on Functions and Changing their Implementation, so I suggest starting there.


Whether we’re testing server or browser code, both of these are using a module system. We can take advantage of this by mocking certain dependencies during testing.

We often don’t want some of our modules to do what they normally do. For example, we don’t want to make an actual API request, instead we want to mock that implementation in a way that will make our code work without unwanted functionality.

Let’s say that the head of the Ministry of Silly Walks wanted to create a method for plotting their walking pattern as an array of steps using left and right legs:

// monty-python.js
module.exports = class MontyPython {
  getSillyWalk(numberOfSteps) {
    const steps = []
    for (let i = 0; i < numberOfSteps; i++) {
      if (steps[steps.length - 1] !== 'left') {
        steps.push('left')
      } else {
        steps.push('right')
      }
    }
    // shuffle
    return steps.sort(() => 0.5 - Math.random())
  }
}

Since this is randomized functionality, we have to mock its implementation if we need predictable behavior in our tests.

There are three types of mocking modules. Let’s start from local to global:

Mocking per test

Sometimes you want to implement a certain modules differently multiple times within the same file. In this case you should use jest.doMock followed by requiring affected modules.

describe('MontyPython', () => {
  describe('getSillyWalk', () => {
    it('returns a series of steps for each leg', () => {
      jest.doMock('./monty-python', () => {
        return class MontyPython {
          getSillyWalk() {
            return [
              'left', 'right',
              'left', 'right',
              'left', 'right',
            ]
          }
        }
      })
      const MontyPython = require('./monty-python')
      const montyPython = new MontyPython()
      expect(montyPython.getSillyWalk(6)).toMatchSnapshot()
      jest.resetModules()
    })
  })
})

To prevent tests from affecting each other, make sure to clean up by call jest.resetModules. I usually put this in afterEach, just so I don’t have to always remember to do it, just like cleanup in react-testing-library.

Mocking per file

While jest.doMock can also be used on a per file basis, I recommend using the top-level jest.mock instead. Use it when you need the same mocked implementation across multiple tests in the same file.

This is a special utility that gets hoisted to the top, before all import statements and require calls.

const MontyPython = require('./monty-python')

jest.mock('./monty-python', () => {
  return class MontyPython {
    // mocked implementation
  }
})

☝️ The code above actually runs in the reverse order:

jest.mock('./monty-python', () => {
  return class MontyPython {
    // mocked implementation
  }
})

const MontyPython = require('./monty-python')

So the imported MontyPython class will be the one you provided as mocked implementation (a.k.a. factory) in the jest.mock call. Keep this in mind to avoid unexpected behavior. Sometimes errors will remind you about this, e.g. if you try to do funny business like this:

const { meaningOfLife } = require('./consts')

jest.mock('./monty-python', () => {
  return class MontyPython {
    getTheMeaningOfLife() {
      return meaningOfLife
    }
  }
})

Jest will throw an error and explaning why this won’t work:

babel-plugin-jest-hoist: The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables.
Invalid variable access: meaningOfLife
Whitelisted objects: Array, ArrayBuffer, Boolean, (...)
Note: This is a precaution to guard against uninitialized mock variables. If it is ensured that the mock is required lazily, variable names prefixed with `mock` (case insensitive) are permitted.

Other than this caveat, jest.mock is pretty much the same as jest.doMock, with obvious difference that the scope is now the whole file, not a single test. Also, you don’t need to reset modules because they are being reset automatically for each test file.

If there is a certain test where you want to use the real monty-python module, you can do so using jest.requireActual:

const MockedMontyPython = require('./monty-python')

jest.mock('./monty-python', () => {
  return class MontyPython {
    getTheMeaningOfLife() {
      return 42
    }
  }
})

describe('MontyPython', () => {
  describe('getTheMeaningOfLife', () => {
    it('gets the mocked meaning of life', () => {
      const montyPython = new MockedMontyPython()
      expect(montyPython.getTheMeaningOfLife()).toBe(42)
    })

    it('gets the real meaning of life', () => {
      const RealMontyPython = jest.requireActual('./monty-python')
      const montyPython = new RealMontyPython()
      const mathRandomSpy = jest.spyOn(Math, 'random')
      mathRandomSpy.mockImplementation(() => '¯\_(ツ)_/¯')
      expect(montyPython.getTheMeaningOfLife()).toBe('¯\_(ツ)_/¯')
      mathRandomSpy.mockRestore()
    })
  })
})

Alternatively you can use jest.dontMock, followed by a regular require call:

it('gets the real meaning of life', () => {
  jest.dontMock('./monty-python')
  const RealMontyPython = require('./monty-python')
  // ...
  jest.resetModules()
})

Lastly, passing the implementation to jest.mock is actually optional, I lied by omission! If you don’t pass the implementation, the default behavior replaces all functions in that module with dummy mocks, which I don’t find particularly useful, but things get more interesting when you add a __mocks__ folder. 👇

Mocking globally

Jest calls these “manual mocks”. Personally, I use them rarely, but they’re handy when you want to mock a certain module in multiple test files. You can create them by using the following file structure:

monty-python.js
__mocks__
└── monty-python.js

You place a __mocks__ folder right next to the module you’re mocking, containing a file with the same name. If you’re mocking a module in node_modules or a built-in module like fs or path, then add a __mocks__ folder next to node_modules.

Now when you call jest.mock('./monty-python') without providing an implementation, Jest will use the manual mock, __mocks__/monty-python.js, as the implementation:

module.exports = class MontyPython {
  getTheMeaningOfLife() {
    return 42
  }
}

Manul mocks for node_modules will be used automatically, even without calling jest.mock (this doesn’t apply to built-in modules). Jest documentation presents this behavior as a feature, but I see it as a relic from their former behavior when they were automocking all modules by default.

You can always opt-out from manual mocks in lots of different ways, depending on what you need:

  1. by passing the implementation to jest.mock,

  2. by calling jest.unmock for modules like those in node_modules that would otherwise be mocked automatically,

  3. by calling jest.requireActual or jest.dontMock, if you need to use actual implementation only in particular tests, not the whole file etc.

Conclusion

It took me a long time to understand the nuances of these features, how to get what I want and how to even know what I want. I encourage you to scroll through the jest object reference to learn more about these features and how they compare to the ones that I didn’t cover in this post. If you catch yourself repeating the same module implementation multiple times, try saving some work by using a different mocking approach.

I hope that this post brought you some clarity on the subject, have fun building better tests!

avatar