But really, what is a JavaScript mock?

By Ken C. Dodds

Cut out the first few paragraphs of bragging and get straight to the point

Step 0

To find out what a mock is, you need something to test and mock. Here’s the code we’ll test:

import {getWinner} from './utils'
function thumbWar(player1, player2) {
  const numberToWin = 2
  let player1Wins = 0
  let player2Wins = 0
  while (player1Wins < numberToWin && player2Wins < numberToWin) {
    const winner = getWinner(player1, player2)
    if (winner === player1) {
      player1Wins++
    } else if (winner === player2) {
      player2Wins++
    }
  }
  return player1Wins > player2Wins ? player1 : player2
}
export default thumbWar
Copy the code

It’s a guessing game, two out of three. A function called getWinner is used from the utils library. This function returns the winner or null in the case of a draw. Let’s assume that getWinner is calling some third-party machine learning service, which means our test environment has no control over it, so we need to mock it in our test. This is a situation where you can only reliably test your code by mocking it. (For simplicity, assume that this function is synchronous.)

In addition, other than reimplementing getWinner’s logic, it’s virtually impossible to make a useful judgment call to determine who actually won in a guessing game. There is no mocking test like this. Here is our best test:

There is little use when there is no mocking situation to assert that the winner is one of the other mocking players

import thumbWar from '.. /thumb-war'
test('returns winner', () = > {const winner = thumbWar('Ken Wheeler'.'Kent C. Dodds')
  expect(['Ken Wheeler'.'Kent C. Dodds'].includes(winner)).toBe(true)})Copy the code

Step 1

Mocking is a simple form called monk-patching. Here’s an example:

Monkey patches are local changes to introduced code, but only affect the currently running instance.

import thumbWar from '.. /thumb-war'
import * as utils from '.. /utils'
test('returns winner', () = > {const originalGetWinner = utils.getWinner
  // eslint-disable-next-line import/namespace
  utils.getWinner = (p1, p2) = > p2
  const winner = thumbWar('Ken Wheeler'.'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  // eslint-disable-next-line import/namespace
  utils.getWinner = originalGetWinner
})
Copy the code

Looking at the code above, you can notice the following: 1. We must import utils as import * as so that we can manipulate the object later (more on the downside of this later). 2. We need to save the original value of the function we mock and then restore the original value after the test so that other tests using utils are not affected by the test case.

All of the above is so that we can mock the getWinner function, and the actual mock operation is just one line:

utils.getWinner = (p1, p2) = > p2
Copy the code

This is called the Monkey patch, and so far it has worked (we can now identify a certain winner in the guessing game), but it still leaves a lot to be desired. The first thing that made us sick were these ESLint warnings, so we added a lot of ESLint-Disable (again, don’t do this in your code, we’ll come back to it later). Second, we still don’t know if the getWinner function is called the number of times we expect it to be called (2, best of three). This may not be important for our application, but it is important for the mocks we cover in this article. So, let’s optimize it.

Step 2

Let’s add some code to make sure that the getWinner function is called twice and that the correct arguments are passed in each time.

import thumbWar from '.. /thumb-war'
import * as utils from '.. /utils'
test('returns winner', () = > {const originalGetWinner = utils.getWinner
  // eslint-disable-next-line import/namespace
  utils.getWinner = (. args) = > {
    utils.getWinner.mock.calls.push(args)
    return args[1]
  }
  utils.getWinner.mock = {calls: []}
  const winner = thumbWar('Ken Wheeler'.'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  expect(utils.getWinner.mock.calls).toHaveLength(2)
  utils.getWinner.mock.calls.forEach(args= > {
    expect(args).toEqual(['Ken Wheeler'.'Kent C. Dodds'])})// eslint-disable-next-line import/namespace
  utils.getWinner = originalGetWinner
})
Copy the code

In the above code we added a mock object to hold the metadata generated when the mock function is called. With it, we can make the following two assertions:

expect(utils.getWinner.mock.calls).toHaveLength(2)
utils.getWinner.mock.calls.forEach(args= > {
  expect(args).toEqual(['Ken Wheeler'.'Kent C. Dodds'])})Copy the code

These two assertions ensure that our mock function is called properly (with the correct parameters) and is called the right number of times (2 for best-of-three).

Now that our mock can depict the actual running scene, we can be more informed about our code. The downside is that we have to tell you exactly what the mock function is doing. TODO

Step 3

So far, so good, but the nasty part is that we have to manually add trace logic to log the mock function calls. Jest has this mock feature built in, so let’s use Jest to simplify our code:

import thumbWar from '.. /thumb-war'
import * as utils from '.. /utils'
test('returns winner', () = > {const originalGetWinner = utils.getWinner
  // eslint-disable-next-line import/namespace
  utils.getWinner = jest.fn((p1, p2) = > p2)
  const winner = thumbWar('Ken Wheeler'.'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  expect(utils.getWinner).toHaveBeenCalledTimes(2)
  utils.getWinner.mock.calls.forEach(args= > {
    expect(args).toEqual(['Ken Wheeler'.'Kent C. Dodds'])})// eslint-disable-next-line import/namespace
  utils.getWinner = originalGetWinner
})
Copy the code

Here we just use jest. Fn to wrap the mock function of the getWinner. The basic functionality is similar to the mocks we implemented ourselves before, but with Jest mocks, we can use some of the specified assertions provided by Jest (such as toHaveBeenCalledTines), which is obviously more convenient. Unfortunately, Jest doesn’t provide an API like nthCalledWidth (which seems to be coming soon), or we could have avoided these forEach statements. But even so, all seems well.

Another thing I don’t like is having to manually save Originalgetwinners and then restore them when the test is done. And those pesky ESLint comments (this is important, we’ll get to that in a moment). Next, let’s see if we can simplify our code even further using the tools provided by Jest.

Step 4

Fortunately, Jest has a utility function called spyOn that provides the functionality we need.

import thumbWar from '.. /thumb-war'
import * as utils from '.. /utils'
test('returns winner', () => {
  jest.spyOn(utils, 'getWinner')
  utils.getWinner.mockImplementation((p1, p2) = > p2)
  const winner = thumbWar('Ken Wheeler'.'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  utils.getWinner.mockRestore()
})
Copy the code

Yes, the code is much simpler. Mock functions are also called spy (which is why the API is called spyOn). By default Jest saves the original implementation of getWinner and tracks how it was called. We don’t want the original implementation to be called, so we use mockImplementation to specify what results we should return when we call it. Finally, we use mockRestore to remove the mock operation to keep the getWinner as it should be. (Same thing we did before, right?).

Remember the ESLint error we mentioned earlier, let’s fix that.

Step 5

The ESLint error we encountered is very important. We ran into this problem because the way we wrote our code meant that ESlint-plugin-import couldn’t statically detect if we were breaking its rules. This rule is very important: import/namespace. The reason we break this rule is because we assign values to members of the import namespace.

Why is this a problem? Because our ES6 code was converted by Babel into CommonJS, which has what’s called the require cache. When I import a module, I am actually importing the execution environment of the functions in that module. So when I introduce the same module in different files and try to modify the execution environment, the change only applies to the current file. So if you rely heavily on this feature, you’re likely to run into a pit when upgrading ES6 modules.

Jest emulates a system of modules that makes it easy and seamless to replace our mock implementation with the original one, and now our test looks like this:

import thumbWar from '.. /thumb-war'
import * as utilsMock from '.. /utils'
jest.mock('.. /utils', () = > {return {
    getWinner: jest.fn((p1, p2) = > p2),
  }
})
test('returns winner', () = > {const winner = thumbWar('Ken Wheeler'.'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  expect(utilsMock.getWinner).toHaveBeenCalledTimes(2)
  utilsMock.getWinner.mock.calls.forEach(args= > {
    expect(args).toEqual(['Ken Wheeler'.'Kent C. Dodds'])})})Copy the code

We simply told Jest that we wanted all files to use our mock version. Notice that I have changed the name import to utilsMock. This is not necessary, but I like this way of showing that the import is a mock version and not the original implementation.

FAQ: If you want to mock just one function in a module, you might want to look at the require.RequireActualAPI

Step 6

I’m almost done here. What if we want to use the getWinner function in multiple tests, but don’t want to copy and paste the mock code everywhere? This is facilitated by using the __mocks__ folder. So we create a __mocks__ folder next to the file we want to mock, and then create a file with the same name:

other/whats-a- a mock / ├ ─ ─ __mocks__ │ └ ─ ─ utils. Js ├ ─ ─ __tests__ / ├ ─ ─ thumb - war. Js └ ─ ─ utils. JsCopy the code

In the __mocks__/utils.js file, we write:

// __mocks__/utils.js
export const getWinner = jest.fn((p1, p2) = > p2)
Copy the code

So our test can be written as:

// __tests__/thumb-war.js
import thumbWar from '.. /thumb-war'
import * as utilsMock from '.. /utils'
jest.mock('.. /utils')
test('returns winner', () = > {const winner = thumbWar('Ken Wheeler'.'Kent C. Dodds')
  expect(winner).toBe('Kent C. Dodds')
  expect(utilsMock.getWinner).toHaveBeenCalledTimes(2)
  utilsMock.getWinner.mock.calls.forEach(args= > {
    expect(args).toEqual(['Ken Wheeler'.'Kent C. Dodds'])})})Copy the code

Now we just need to write jest. Mock (pathToModule), which will automatically use the mock implementation we just created.

We might not want our mock implementation to always return the second player winning, so we can use mockImplementation to give the desired implementation for a particular test, and then test whether the other cases pass. You can also use a few library methods in your mocks as much as you like.

End.