Duck Who Codes's logo

A short note about mocking in Jest

The situation

Suppose we have the following code.

const { Storage } = require('@google-cloud/storage')

const storage = new Storage()

const getFile = (bucketName, filename) => {
  const bucket = storage.bucket(bucketName)
  return bucket.file(filename) // this makes an API request and returns a Promise
}

Since we wouldn’t like our tests to hit the Cloud Storage API (that’s slow and sometimes flaky), we need to mock the @google-cloud/storage module. There are a few ways we can do that.

__mocks__ directory

With this kind of mocking, we create a mock module inside the __mocks__ directory. Where to create this directory depends on what kind of module we’re mocking. @google-cloud/storage is a scoped Node module so we need to create a directory structure like this:

<root_dir>/__mocks__/@google-cloud/storage.js

The storage.js file is our mock file. We replace the Storage class with our own implementation.

const cloudStorage = jest.createMockFromModule('@google-cloud/storage')

cloudStorage.Storage = class {
  bucket(bucketName) {
    const bucket = {
      file: (filename) => {
        const fakeFile = {
          // ...
        }
        return Promise.resolve(fakeFile)
      }
    }
    return bucket
  }
}

module.exports = cloudStorage

In our test files, we don’t need to explicitly call jest.mock('@google-cloud/storage'). The module is automatically mocked. This behavior can be turned off by using jest.unmock().

const { getFile } = require('./storage')

test('can return a file at a specific bucket', async () => {
  // call `getFile`
  // test assertions
})

However, if we mock a user-written module or a built-in Node.js module, calling jest.mock() is required.

jest.mock()’s factory function

jest.mock() has a second argument that accepts a factory function. When provided, Jest will replace the actual module with whatever this factory function returns.

const mockedStorage = ({ fakeFile }) => {
  return class {
    bucket(bucketName) {
      const bucket = {
        file: (filename) => {
          return Promise.resolve(fakeFile)
        }
      }
      return bucket
    }
  }
}

test('can return a file at a specific bucket', async () => {
  const fakeFile = {
    // ...
  }

  jest.mock('@google-cloud/storage', () => ({
    Storage: mockedStorage({ fakeFile })
  }))

  const { getFile } = require('./storage')
  // call `getFile`
  // test assertions
})

Notice that the require statement must be called after jest.mock().

This kind of mocking is intended to be used when we want to mock a module differently within the same file. By making the mock generating code into a separate function, different test cases can get different mocks that behave as expected with the given input.

A small tip when writing mocks

A mock module doesn’t have to implement everything the original module has. It should be as simple as possible — only contains the necessary parts to make the code that we’re testing run.

References: