Mocking

Prerequisites

Before we go into mocking, you must first accept that functions are in fact a special type of object. Since functions are actually objects, they also have properties like a regular object.

const a = () => {
a.count = (a.count || 0) + 1
console.log(`count is currently ${a.count}`)
}
a() // will log 1
a() // will log 2
a() // will log 3
a() // will log 4
console.log(a.count) // will log 4
// You can directly set properties to a function
a.count = 99
a() // will log 100

Mock function

A mock function is a fake function that doesn't do anything but keep track of how many times it called. It also keeps track of the arguments that it is being called with. You can create a mock function by calling jest.fn function.

The mocked function contains an array of all the times the function has been called. You can access this array like this:

// a is a mock function.
const a = jest.fn()
a() // a is run without any arguments.
console.log(a.mock.calls.length) // a.mock.calls is an array of length 1
a('hello', 'world') // a is run with 2 arguments.
console.log(a.mock.calls.length) // a.mock.calls is an array of length 2
a('test', 1, 9, 100) // a is run with 4 arguments.
console.log(a.mock.calls.length) // a.mock.calls is an array of length 3
a(() => {
console.log('hello')
}) // a is run with 1 argument
console.log(a.mock.calls.length) // a.mock.calls is an array of length 4
console.log(a.mock.calls)
/*
[
[],
['hello', 'world'],
['test', 1, 9, 100],
[ function ]
]
*/
// a.mock.calls[0] is empty array because the first call has no arguments.
// a.mock.calls[1] contains array of arguments passed into the function.
// a.mock.calls[2] contains an array of arguments passed into the function.
// a.mock.calls[3] contains an array of arguments passed into the function.
a.mock.calls[3][0]() // prints out 'hello'

Mock Return

If you want a more complicated mock function that can return something, you can pass in a function. In the example below, nothing changes except your mocked function now returns a value! It can actually do something now.

const a = jest.fn((a, b) => {
return a + b
})
console.log(a(6, 7)) // prints out 13
console.log(a.mock.calls)
/*
[
[6,7]
]
*/

Example - console.log

We are testing a function that takes in a number and prints out "hello" that many times.

// solution.js
const logHello = i => {
if (i <= 0) return
console.log('hello')
return logHello(i - 1)
}
module.exports = logHello

To test if the above function is working, I need to make sure that

  1. logHello(-10) - console.log is called 0 times.
  2. logHello(0) - console.log is called 0 times.
  3. logHello(40) - console.log is called 40 times.

I want to observe how many times console.log is being called, so instead of the original function, I should replace it with a mock function.

// solution.test.js
const logHello = require('./solution.js')
describe('logHello', () => {
test('should log 0 times if -10 is passed in', () => {
console.log = jest.fn()
logHello(-10)
expect(console.log.mock.calls.length).toEqual(0)
})
test('should log 0 times if 0 is passed in', () => {
console.log = jest.fn()
logHello(0)
expect(console.log.mock.calls.length).toEqual(0)
})
test('should log hello 3 times if 3 is passed in', () => {
console.log = jest.fn()
logHello(3)
expect(console.log.mock.calls.length).toEqual(3)
// first argument in the first call should be hello.
expect(console.log.mock.calls[0][0]).toEqual('hello')
// first argument in the second call should be hello.
expect(console.log.mock.calls[1][0]).toEqual('hello')
// first argument in the third call should be hello.
expect(console.log.mock.calls[2][0]).toEqual('hello')
})
})

Mock modules

Sometimes, the functions you are testing will depend on external modules that you have no control over. To mock an entire module, you run jest.mock('module-name')

In the example below, the function first sends a request to an API to get an array of objects. It will console.log the title for each title property of the object in the array.

// solution.js
const request = require('request')
const logTitles = () => {
request('https://c0d3.com/api/lessons', (err, response, body) => {
const arr = JSON.parse(body)
arr.forEach(lesson => {
console.log(lesson.title)
})
})
}
module.exports = logTitles

To test if the above function is working, I need to make sure that

  1. if request to the API sends back an array string of 0 elements, console.log is called 0 times.
  2. if request to the API sends back an array string of 2 elements, console.log is called 2 times.

Unlike the previous example, this one involves a library that we have no control over, the request library.

We need to overwrite the response so that we can simulate how many elements the API sends back.

// solution.test.js
jest.mock('request') // very first thing to do, mock the library
// when this happens, this file will get the fake request library
const logTitles = require('./solution.js')
// We need to change the mocked library so we can send back fake data
const request = require('request')
// solution.test.js
const logHello = require('./solution.js')
describe('logHello', () => {
test('should call request 1 time', () => {
request.mockClear() // we should always reset the mock of modules
console.log = jest.fn()
logTitles()
expect(request.mock.calls.length).toEqual(1)
expect(console.log.mock.calls.length).toEqual(0)
})
test('should log 0 times if response is []', () => {
request.mockClear() // we should always reset the mock of modules
// If we don't reset the mock, it will include the calls from
// the previous test.
// we need to stringify it because request callback expects a string
console.log = jest.fn()
const fakeResponse = JSON.stringify([])
logTitles()
// request is called 1 time with 2 arguments.
// The second argument is a function. Let's run that function
request.mock.calls[0][1](null, null, fakeResponse)
expect(console.log.mock.calls.length).toEqual(0)
})
test('should log 2 times if response has 2 objects', () => {
request.mockClear() // we should always reset the mock of modules
// we need to stringify it because request callback expects a string
console.log = jest.fn()
const fakeResponse = JSON.stringify([
{
title: 'hello'
},
{
title: 'world'
}
])
logTitles()
// request is called 1 time with 2 arguments.
// The second argument is a function. Let's run that function
request.mock.calls[0][1](null, null, fakeResponse)
expect(console.log.mock.calls.length).toEqual(2)
expect(console.log.mock.calls[0][0]).toEqual('hello')
expect(console.log.mock.calls[1][0]).toEqual('world')
})
})
Edit this page on Github