How does Jest work inside?

Flávio H. Freitas
DailyJS
Published in
3 min readMar 12, 2020

One of these days, I got myself thinking about the magic inside jest (our beloved JavaScript testing framework). How do the functions test, expect, mock, and spyOn work?

I tried then to replicate it and create my own implementation of Jest functions, we will name it flaviest 😂. Let’s see the magic:

Test function

Let’s start with the basic structure of a test, the test method itself. I need to be able to write a test implementation, give it a title, and flag if it went well or not.

We wanted to have something like this:

test('Passing test', () => {})
// ✓ Passing test

and:

test('Failing test', () => new Error('Error message'))
// ✕ Failing test
// Error message

So we can implement:

function test(title, callback) {
try {
callback()
console.log(`✓ ${title}`)
} catch (error) {
console.error(`✕ ${title}`)
console.error(error)
}
}

Expect

Now we need to be able to assert the values we want to test. For simplification, we will just show the toBe function.

We will test the following function:

function multiply(a, b) {
return a * b
}

And we want to test:

test('Multipling 3 by 4 is 12', () => {
expect(multiply(3, 4)).toBe(12)
}) // ✓ Multipling 3 by 4 is 12
test('Multipling 3 by 4 is 12', () => {
expect(multiply(3, 4)).toBe(13)
}) // ✕ Multipling 3 by 4 is 12
// Expected: 13
// Received: 12

We could have:

function expect(current) {
return {
toBe(expected) {
if (current !== expected) {
throw new Error(`
Expected: ${expected}
Received: ${current}
`)
}
}
}
}

Mock

When we want to avoid running a function that is being called inside another one, we mock it. It means, we override the original implementation. Jest has for this purpose a function called jest.fn (fn from function).

Let’s see how it works:

// random.js
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min) + min)
}
export { getRandom }

and:

// cards.js
import { getRandom } from './random.js'
const getRandomCard = (cards) => {
const randomCardIndex = getRandom(0, array.length)
return cards[randomCardIndex]
}
export { getRandomCard }

we want to test:

// cards.test.js
import * as randomGenerator from './random.js'
import { getRandomCard } from './cards.js'
test('Returns 7♥', () => {
const originalImplementation = randomGenerator.getRandom
randomGenerator.getRandom = jest.fn(() => 2) const result = getRandomCard(['2♣', 'K♦️', '7♥', '3♠'])
expect(result).toBe('7♥')
expect(randomGenerator.getRandom).toHaveBeenCalledTimes(1)
expect(randomGenerator.getRandom).toHaveBeenCalledWith(0, 4)
// we keep the test idempotent
randomGenerator.getRandom = originalImplementation
})

To make this possible, we need jest.fn:

function fn(impl) {
const mockFn = (...args) => {
mockFn.mock.calls.push(args)
return impl(...args)
}
mockFn.mock = {calls: []}
return mockFn
}

and we implement:

import assert from 'assert'function expect(current) {
return {
toHaveBeenCalledTimes(nrTimesExpected) {
if (current.mock.calls.length !== nrTimesExpected) {
throw new Error(`
Expected: ${expected}
Called: ${func.mock.calls.length}
`)
}
},
toHaveBeenCalledWith(...params) {
// this is a simplified version
if (!assert.deepStrictEqual(current.mock.calls[0], ...params)) {
throw new Error(`
Expected: ${expected}
Called: ${func.mock.calls.length}
`)
}
}
}
}

jest.spyOn

If we want to make sure that our tests are idempotent, we need to restore the mocked function to its original value. With the mock implementation described above, we have to keep the original value and then reapply at the end of the test. Let's try to see how jest.spyOn makes our lives easier.

// cards.test.js
import * as randomGenerator from './random.js'
import { getRandomCard } from './cards.js'
test('Returns 7♥', () => {
jest.spyOn(randomGenerator, 'getRandom')
randomGenerator.getRandom.mockImplementation(() => 2)
const result = getRandomCard(['2♣', 'K♦', '7♥', '3♠'])
expect(result).toBe('7♥')
expect(randomGenerator.getRandom).toHaveBeenCalledTimes(1)
expect(randomGenerator.getRandom).toHaveBeenCalledWith(0, 4)
// we restore the implementation and keep the test idempotent
randomGenerator.getRandom.mockRestore()
})

And for this to be possible, we can think of:

function fn(impl = () => {}) {
const mockFn = (...args) => {
mockFn.mock.calls.push(args)
return impl(...args)
}
mockFn.mock = {calls: []}
mockFn.mockImplementation = newImpl => (impl = newImpl)
return mockFn
}

and

function spyOn(obj, prop) {
const originalValue = obj[prop]
obj[prop] = fn()
obj[prop].mockRestore = () => (obj[prop] = originalValue)
}

And that's it. Jest is not a mystery anymore. I hope you had fun with this article as much as I had while thinking and researching about it.

Follow me if you want to read more of my articles 😘 And if you enjoyed this article, be sure to like it give me a lot of claps — it means the world to the writer.

Flávio H. de Freitas is an Entrepreneur, Engineer, Tech lover, Dreamer, and Traveler. Has worked as CTO in Brazil, Silicon Valley, and Europe.

--

--