Day 2 Overview: Mocks

Day 2 Overview: Mocks

Mocks


Monkey Patch a Mock Exercise

For this next section on mocks, I'll use another example to start formulating the concept of what mocks are and when they should be used. Let’s check out the following code to start the process:

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

This code simply imports a getWinner() function, and then uses a while loop, with the condition of only stopping when either player1 or player2 has more than the numberToWin . It utilizes the getWinner() function to set the winner on each iteration and then returns the player that reaches the numberToWin first.

So in the context of my tests, I'm going to assume that the module I'm importing, getWinner, is unreliable in my testing env. In addition when unit testing, it seems that I shouldn't be making any network requests. Now, I’ve heard this everywhere before, and in fact, is a big reason why writing tests has been such a pain in my VanillaJS Todo app, but it’s never been made clear as to WHY I shouldn’t be making network requests.

Regardless, I'll be moving from a less optimal way of mocking and optimizing along the way to building an intuition around mocking. Hopefully, I also get some insight as to why I even need to mock at all.

So let’s check out the below code:

// monkey-patching
import thumbWar from '../thumb-war'
import * as utils from '../utils'
// import the utils module (see hint #1 at the bottom of the file)

test('returns winner', () => {
  const originalGetWinner = utils.getWinner
  // keep track of the original `getWinner` utility function (see hint #2)
  // overwrite the utils.getWinner function with
  // our own that always returns the second player (see hint #3)
  utils.getWinner = (p1, p2) => p2

  const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
  // change this assertion to be more for a specific player
  // (like 'Kent C. Dodds', see hint #4):
  expect(winner).toBe('Kent C. Dodds')
  // restore the originalGetWinner function so other tests don't break
  // (see hint #5)

  utils.getWinner = originalGetWinner
})

So what is monkey-patching? When I googled the term, I got a pretty vague and general definition: “Monkey patching is a technique used to dynamically update the behavior of a piece of code at run-time. A monkey path is a way to extend or modify the runtime code of dynamic languages without altering the original source code.”

While the definition made little sense to me when I first read it, after doing the exercise it became pretty straightforward that this first suboptimal task was using monkey patching. To dig in, I import the utils function and then save that function to a variable called originalGetWinner, I did this so that I could then overwrite the utils.getWinner function with my own that will always return the second player. But why do I have to do this? Again, so that I don’t use the function that has some network request or other unreliable testing environment that I have no control over.

So, back to my example, I overwrite the utils.getWinner and then assert to check that I get back a specific player. Lastly, I restore the utils.getWinner function by reassigning it to the first originalGetWinner variable.

I can see why this technique isn’t optimal because apart from overwriting an import (which would either throw a syntax or runtime error if using a native dependency in something like Node), it can be very easy to simply forget to restore the mocked function and ruin future tests. Additionally, as soon as I mock something, I'm are severing any confidence that I have in the integration between the fake thing and whatever I'm testing. For example, let’s say someone changes the implementation of the getWinner function to take in an additional parameter, or change the parameters set— my test would still pass since I'm mocking different functionality and I'd run into problems not knowing if I'm calling something properly.

However, there are valid reasons why I would need to mock something, ie: testing functionality where I might need to charge an account– that’s just not feasible to test over and over. So if I could somehow restore that confidence in my mocks, that would be very helpful— which is something called contract testing. Unfortunately, this is out of scope of this course, but we’ll cover something similar.

Asserting Calling a Mock Exercise

For this next exercise, I change the implementation of the utils.getWinner function again, but add 2 new things: the ability to keep track of how often it’s called and the arguments it’s called with. This allows the ability to assert how many times it’s been called and that it’s called with the arguments I specify:

// improved assertions for mocks
import thumbWar from '../thumb-war'
import * as utils from '../utils'

test('returns winner', () => {
  const originalGetWinner = utils.getWinner
  // change the getWinner implementation to a function
  // that keeps track of how often it's called and
  // the arguments it's called with (Hint #1)

  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')
  // add an assertion for how many times the getWinner function
  // was supposed to be called (2 times) (Hint #2)
  expect(utils.getWinner.mock.calls).toHaveLength(2)
  // add another assertion that every time it was called
  // it was called with the right arguments: 'Ken Wheeler', 'Kent C. Dodds'
  // (Hint #3)
  utils.getWinner.mock.calls.forEach((args) => {
    expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
  })

  utils.getWinner = originalGetWinner
})

While this is better than my previous implementation, there’s still the potential bug creator of overwriting and reassigning the originalGetWinner function. However, most of the new logic I've implemented is similar to the API Jest uses in their spyOn functions. So let’s see how we can implement a spyOn for our use case instead.

Using Jest spyOn

So let’s see how the spyOn mock utility works:

// using jest utilities
import thumbWar from '../thumb-war'
import * as utils from '../utils'

test('returns winner', () => {
  // replace these lines with a call to jest.spyOn and
  // call to mockImplementation on the mocked function (See hint #1)

  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.mock.calls.forEach((args) => {
    expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
  })

  // replace the next two lines with a restoration of the original function
  // (See hint #2)
  utils.getWinner.mockRestore()
})

So here I start by calling the jest.spyOn() utility, which simply decorates the original utils function and expects me to pass in the utils object and then call the method mockImplementation() supplied by Jest. I then pass my own implementation into mockImplementation() as an argument. Additionally, I simply have to call the mockRestore() method at the end of the test to restore the original implementation.

Using Jest Mock

However, even though Jest is now responsible for modifying the utils object, it’s not really an object, it’s more of an imported namespace and modifying namespaces isn’t recommended, (which is why ESLint in my VSCode had red squigglies when I was modifying it manually in the first two iterations of the exercise). Turns out Jest has a mock() mechanism that helps solve this specifically.

Let’s take a look at an example:

// avoid monkey-patching with jest.mock
import thumbWar from '../thumb-war'
import * as utils from '../utils'

// add an inline mock with the jest.mock API
//
// jest.mock(
//   relativePathToModuleToMock,
//   functionThatReturnsMockObject
// )
//
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(utils.getWinner).toHaveBeenCalledTimes(2)
  utils.getWinner.mock.calls.forEach((args) => {
    expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
  })

  // remove the next line
  // utils.getWinner.mockRestore()
})

So what’s going on here?

  • jest.mock(relativePathToModule, functionThatReturnsMockObject) takes the relative path to the module, like importing it before, and a function that mocks the module function

    • I can think about the functionThatReturnsMockObject as an entirely new module, and instead, Jest will treat it as a regular module, but it’s really an object I'm returning. And since Jest takes control of the module system in Node, whenever any part of our code tries to import the module we’ve defined in our function, Jest will swap out the real module and replace it with what we’ve defined. Basically, whenever we do a require statement or import it’s going to go through Jest first, check its registry of mocks, and if it finds a match, Jest will use our mock instead.

      💡
      This only applies to the test files.
    • jest.fn() An alternative to jest.spyOn that implements our mock implementation.

Now I don’t need to manually overwrite the functions OR restore them at the end since Jest takes care of all of this for me.

Additionally, if I wanted to use other properties and just overwrite one property within the module, I could do that as well:

jest.mock('../utils', () => {
    const actualUtils = require.requireActual('../utils');
    return {
            ...actualUtils,
            getWinner: jest.fn((p1, p2) => p2));
    }
});

One more thing to note is that tests should be in complete isolation from each other. For example, if I were to copy and paste my test twice, and run them, I would get an error saying the mock function has been called 4 times, instead of the 2 that I had indicated:

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(utils.getWinner).toHaveBeenCalledTimes(2)
  utils.getWinner.mock.calls.forEach((args) => {
    expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
  })
})

test('returns winner', () => {
  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'])
  })
})

To prevent this from happening, I can simply use the utils.getWinner.mockClear() function before each test, however, since I'll be using this for each test case in this file, instead of copying and pasting the same line multiple times I can utilize beforeEach():

beforeEach(() => {
    utils.getWinner.mockClear();
})

Now that I've successfully used mocks for this particular function, what if I have a module that I want to mock across multiple test files? I could simply copy and paste the jest.mock() function in those different files, but there’s a better way to do this, by using a __mocks__ directory, naming the file the same I use it in the mock and using jest.mock() without having to pass it in the callback function since it’s already in our __mock__ directory:

jest.mock('../utils')

Often the things we want to mock will be in our node_modules like Axios:

import axiosMock from 'axios';

beforeEach(() => {
    axiosMock.post.mockClear();
})

Using a mocks directory

Generally, when testing in a new project or module, the best path is the path of least resistance. Maybe look for the simplest function and start there. Similar to how I went about converting a VanillaJS application into a React one. For example, say I have a users.js file that will handle the logic for auth for users to see who can get users:

//users.js
async function getUsers(req, res) {
    const users = await db.getUsers();
    if(users) {
        return res.json({users: users.map(u => userToJSON(u))})
    } else {
        return res.status(404).send();
    }
}

Let’s do a happy path test first:

//users.test.js
import * as usersController from '../users';
test('getUsers gets all users from a db', async() => {
    const req = {};
    const res = {
        json: jest.fn();
    };

    await usersController.getUsers(req, res);

    expect(res.json).toHaveBeenCalledTimes(1);

    //we can also jest to see what our mock function was called with:
    console.log(res.json.mock.calls[0]);
})

So let’s see what’s going on here:

  • First I need to import my function, so I import everything in the file as usersControllers and then use dot notation to grab the function I want, in this case: getUsers()

  • Next, I set up the test() and set the req as an empty object and res as an object that will have a json() property which I'll mock implement with jest.fn().

    • I do this so that I can pass the req and res objects to the usersController.getUsers() function
  • Now I simply assert that the res.json function was called the appropriate amount of times!

💡
"From the purest sense, a Unit Test mocks all dependencies and only tests the things that are in the module and could be argued that it’s not an integration test. Doesn’t matter the technicality, the most important thing is to have firm confidence that your tests are replicating how your software is used." - KCD

However if I console log what the actual mock was called with, I see that the DB hasn’t been initialized. If I wanted to do that, I should probably mock out the DB init as well.

More Mocking with users

Now let’s look at a similar example but with more users, say because I utilized a mocking function that generates a random number of users that are always different:

const safeUser = u => omit(u, ['salt', 'hash']);

beforeEach(() => initDb());

test('getUsers returns all users in the db', async() => {
    const req = {};
    const res = {
        json: jest.fn();
    };

    await usersController.getUsers(req, res);

    expect(res.json).toHaveBeenCalledTimes(1);
    const firstCall = res.json.mock.calls[0];
    const firstArg = firstCall[0];
    const {users} = firstArg;
    expect(users.length).toBeGreaterThan(0);
    const actualUsers = await db.getUsers();
    expect(users).toEqual(actualUsers.map(safeUser));
})

With this code, I can destructure the first call to the res.json function, and the firstArg as well to make sure that the amount of users I received is greater than 0.

That's all for today! Next time I'll be looking to wrap up the course and dive into Testing Practices and Integrations Tests.