Introduction to Jest mocking (Jest mocking + React part 1)

Peter Jacxsens - Sep 8 '22 - - Dev Community

This series is an introduction into mocking with Jest. You don't need any prior knowledge about mocking to be able to follow this but you should at least know what a Jest test is and how to set one up. I use a lot of examples to make everything as clear as I can but it remains a hefty read.

This first part explains what mocking is, why you need it and how to setup mocking functions. The examples use plain old javascript.

  1. What is a Jest mock?
  2. Relevant matcher functions
  3. Adding return values to mocks

In the next parts we apply what we learned to test React components.

The examples I use in this article are available on github (src/part1). These files a build upon create-react-app so you can run them using npm start or run the tests using npm test.

1. What is a Jest mock?

A mock is a simulated function that records or logs it's own behaviour. It is a simulation because it doesn't actually do anything, it doesn't have a function body. A mock can take arguments and return values but there is nothing in between. Hence, it is a simulation.

A mock also 'logs' it's behaviour. This means that for each time a mock is called, it saves the arguments it was called with as well as the values it returned. As a tester, you can access these 'logs' and use them in your tests. I will use an example to make this clear.

// part1/doSomething.js
function doSomething(callback){
  const list = ['foo', 'bar']
  list.forEach(listItem => callback(listItem))
}
export default doSomething
Enter fullscreen mode Exit fullscreen mode

We have a little function doSomething() that takes a function/callback as an argument. When it runs, it calls that callback on each item in the list array.

function callback(item){
  console.log(item)
}
doSomething(callback)
// prints in console: 
// 'foo'
// 'bar'
Enter fullscreen mode Exit fullscreen mode

We want to test the doSomething() function but we don't have controle over the callback function. It might be anything. However, what we do know is how the callback should behave:

  • Callback should be called twice.
  • Callback should be called the first time with one argument 'foo'.
  • Callback should be called the second time with one argument 'bar'.

Instead of using an actual callback function, we will use a mock. This mock allows us to make these assertions. So, mocking lets you test a function without an actual callback. And that is exactly what we want. Here is the test:

// part1/__tests__doSomething.test.js
test('doSomething callback behaves correctly', () => {
  const mockCallback = jest.fn()
  doSomething(mockCallback)
  expect(mockCallback.mock.calls).toHaveLength(2)
  expect(mockCallback.mock.calls[0][0]).toBe('foo')
  expect(mockCallback.mock.calls[1][0]).toBe('bar')
})
Enter fullscreen mode Exit fullscreen mode

Let's break this test down. First we create a new mock. A mock is created by simply calling jest.fn(). We then run the doSomething() function passing in the mock: doSomething(mockCallback).

doSomething() takes the mock function as a callback and then calls each item in the list array with this callback: callback('foo'), callback('bar').

We used a mock function. Mock functions log what happens to them, how they are used. How do we access this log? We take the mock we made (mockCallback) and look for a .mock property: mockCallback.mock. This might be a bit confusing, the mock mockCallback has a property .mock but that's just how it is.

This mock property is an object and it's two main properties are .calls and .results. The calls property is a 'log' of what the mock function was called with. The results property is a 'log' of the return values of the callback (more on that later). If you console.log mockCallback.mock after it was called by doSomething() it would look like this:

// console.log(mockCallback.mock)
{
  calls: [
    ['foo'],
    ['bar'],
  ],
  results: [
    { type: 'return', value: undefined },
    { type: 'return', value: undefined },
  ],
  // ... more props
}
Enter fullscreen mode Exit fullscreen mode

mockCallback was called twice, so both .calls and .results have 2 items. The first item in the calls array represent the first call, the second item represents the second call. Same goes for the results array.

mockCallback.mock.calls[0] is in itself again an array representing the number of arguments. Since we called mockCallback with only one argument 'foo', the array has only one item. Had mockCallback been called with more arguments, it would look like this: calls[0] = ['argument1', 'argument2', ...].

Now, let's look back at the expect() statements in the above test. These should make sense now.

// part1/__tests__/doSomething.test.js
test('doSomething callback behaves correctly', () => {
  const mockCallback = jest.fn()
  doSomething(mockCallback)
  expect(mockCallback.mock.calls).toHaveLength(2)
  expect(mockCallback.mock.calls[0][0]).toBe('foo')
  expect(mockCallback.mock.calls[1][0]).toBe('bar')
})
Enter fullscreen mode Exit fullscreen mode
  • expect(mockCallback.mock.calls).toHaveLength(2) because mockCallback was called twice.
  • expect(mockCallback.mock.calls[0][0]).toBe('foo'): we expect that the first time mockCallback was called, it's first argument was 'foo'.
  • expect(mockCallback.mock.calls[1][0]).toBe('bar'): we expect that the second time mockCallback was called, it's first argument was 'bar'.

Let's go over this whole mocking thing once more. A mock is a simulation of a function that logs it's own behaviour.

  • It's a simulation of a function because in itself, a mock doesn't do anything.
  • It logs it's behaviour: for each time a mock is called, it saves both its arguments and results.

To make a mock you just use const myMock = jest.fn(). You can access the arguments on myMock.mock.calls and the return values on myMock.mock.results.


2. Relevant matcher functions

Checking mockFunction.mock.calls[1][0] is a bit messy. Luckily, Jest has some helpers for this. Here are a few:

  • .toHaveBeenCalled(): the mock was called at least once.
  • .toHaveBeenCalledTimes(): how many times the mock was called.
  • .toHaveBeenCalledWith(): the mock was called with these arguments at least once.
  • .toHaveBeenNthCalledWith(): the mock was called with these arguments the nth time it got called.
  • .toHaveBeenLastCalledWith(): the mock was called with these arguments the last time it got called.

See them in action on our doSomething() example:

// part1/__tests__/doSomething.test.js
test('doSomething with Jest matchers', () => {
  const mockCallback = jest.fn()
  doSomething(mockCallback)

  expect(mockCallback).toHaveBeenCalled()
  expect(mockCallback).toHaveBeenCalledTimes(2)
  // this is not the recommended method when testing multiple calls
  expect(mockCallback).toHaveBeenCalledWith('foo')
  expect(mockCallback).toHaveBeenCalledWith('bar')
  // this is the recommended method when testing multiple calls
  expect(mockCallback).toHaveBeenNthCalledWith(1, 'foo')
  expect(mockCallback).toHaveBeenNthCalledWith(2, 'bar')
  expect(mockCallback).toHaveBeenLastCalledWith('bar')
})
Enter fullscreen mode Exit fullscreen mode

As you can see, these Jest matchers are a lot cleaner. But, the .mock property on the mock function is still important:

  • There will be situation where you need to use it.
  • I feel it gives a deeper understanding how mocks work internally.

3. Adding return values to mocks

Mock function are simulations that don't return any value. But what do you do when you actually need them to return something? For example when the function you are testing relies on the mocked function to get data. This section covers how to add return values to mocks.

A quick remark first: all javascript functions that have no explicit return, return undefined. The same goes for mocks. Let's test this:

// part1/__tests__doSomething.test.js
test('doSomething with no return value', () => {
  const mockCallback = jest.fn()
  doSomething(mockCallback)
  expect(mockCallback.mock.results[0].value).toBeUndefined()
  expect(mockCallback.mock.results[1].value).toBeUndefined()
})
Enter fullscreen mode Exit fullscreen mode

Before we add return values to our mocks, I'm gonna introducer some matcher functions to test return values. We will use these to test the return values on our mocks. They should look familiar:

  • .toHaveReturned()
  • .toHaveReturnedTimes(number)
  • .toHaveReturnedWith(value)
  • .toHaveLastReturnedWith(value)
  • .toHaveNthReturnedWith(nthCall, value)

There are a number of ways to add return values to mock function. The simplest one is to call jest.fn() with a function as argument. This function is called a mock implementation parameter: jest.fn(implementation) because it's a function that returns a mock. Our example will make this clear:

// part1/__tests__/doSomething.test.js
test('doSomething with mock implementation parameter', () => {
  // pass mock implementation function into jest.fn
  const mockCallback = jest.fn(() => 'Hello')
  doSomething(mockCallback)

  // using .mock property
  expect(mockCallback.mock.results[0].value).toBe('Hello')
  // equivalent using matcher
  expect(mockCallback).toHaveNthReturnedWith(1, 'Hello')
  // using .mock property
  expect(mockCallback.mock.results[1].value).toBe('Hello')
  // equivalent using matcher
  expect(mockCallback).toHaveNthReturnedWith(2, 'Hello')
})

Enter fullscreen mode Exit fullscreen mode

Our implementation is just an anonymous arrow function that we pass into jest.fn(). Every time mockCallback is called, it returns 'Hello'.

In our doSomething() function, our mocks get called with arguments: 'foo' and 'bar'. This is how we catch those values:

// part1/__tests__/doSomething.test.js
test('doSomething with mock implementation parameter that catches the values passed into mockCallback', () => {
  const mockCallback = jest.fn((value) => 'Hello ' + value)
  doSomething(mockCallback)
  expect(mockCallback).toHaveNthReturnedWith(1, 'Hello foo')
  expect(mockCallback).toHaveNthReturnedWith(2, 'Hello bar')
})
Enter fullscreen mode Exit fullscreen mode

More ways to return values from mock are provided by Jest helpers. When you create a jest.fn() you get access to a series of methods. Some of these methods help you with returning values from mocks. We start with .mockReturnValue(value).

// part1/__tests__/doSomething.test.js
test('doSomething with .mockReturnValue() on mock', () => {
  const mockCallback = jest.fn()
  mockCallback.mockReturnValue('Hello')
  // you can chain these methods
  // const mockCallback = jest.fn().mockReturnValue('Hello')
  doSomething(mockCallback)
  expect(mockCallback).toHaveNthReturnedWith(1, 'Hello')
  expect(mockCallback).toHaveNthReturnedWith(2, 'Hello')
})
Enter fullscreen mode Exit fullscreen mode

Then there is .mockReturnValueOnce(). This one is a bit weird because there are some edge cases. We will use a new example function doSomethingElse() to demonstrate. This new function is the same as doSomething() except for other and more items in the list array.

// part1/doSomethingElse.js
// instead of 'foo' and 'bar' we call the mock 4 times now
function doSomethingElse(callback){
  const list = ['a', 'b', 'c', 'd']
  list.forEach(item => callback(item))
}
Enter fullscreen mode Exit fullscreen mode

The test:

// part1/__tests__/doSomethingElse.test.js
test('doSomethingElse with .mockReturnValueOnce() on mock', () => {
  const mockCallback = jest
    .fn()
    .mockReturnValue('default')
    .mockReturnValueOnce('value 1')
    .mockReturnValueOnce('value 2')
  // call the function we are testing
  doSomethingElse(mockCallback)

  // test the return values
  expect(mockCallback).toHaveNthReturnedWith(1, 'value 1')
  expect(mockCallback).toHaveNthReturnedWith(2, 'value 2')
  expect(mockCallback).toHaveNthReturnedWith(3, 'default')
  expect(mockCallback).toHaveNthReturnedWith(4, 'default')
})
Enter fullscreen mode Exit fullscreen mode

A breakdown: first we setup our jest.fn() and then we chain on our methods. We chained two .mockReturnValueOnce() after .mockReturnValue(). Yet, .mockReturnValueOnce() will be called first. This is what the test demonstrates. The once methods are called first and only once.

If you omit .mockReturnValue() then the third and fourth mock calls would just return undefined. So, .mockReturnValueOnce() allows you to setup different return values for each time the mock is called.

The next two methods .mockImplementation() and .mockImplementationOnce() allow you to setup a return function (once). As you would expect, these methods take a function as argument.

// part1/__tests__/doSomething.test.js
test('doSomething with .mockImplementation() on mock', () => {
  const mockCallback = jest
    .fn()
    .mockImplementation((value) => 'Hello ' + value)
  doSomething(mockCallback)
  expect(mockCallback).toHaveNthReturnedWith(1, 'Hello foo')
  expect(mockCallback).toHaveNthReturnedWith(2, 'Hello bar')
})
Enter fullscreen mode Exit fullscreen mode

They are the equivalent of jest.fn(value => 'Hello ' + value). But .mockImplementationOnce() gives you more options. Also note that after an expect statement you can set a new .mockImplementation() statement on your mock and perform a new test. Just mentioning this in case you would need this.


Summary

Jest mocking functions are simulated functions that log their own behaviour. As a tester, you can access these logs to make assertions.

Mock functions can be customized to return the values. There are different ways to do this and they make mock functions very flexible.

This article only tested pure javascript functions. In the next articles in this series I'm gonna show you how to use Jest mocking to test React components.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player