How does Jest work inside?
By: Flávio H. Freitas
Tags: Programming, JavaScript, Front End Development, Web Development, Testing

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.