Mocking JavaScript Class Inner Functions With Jest

I was recently writing unit tests for a project and I wanted to test whether a function in one class had been called by a parent class. Sounds simple right?

I found it a bit trickier than anticipated so thought I’d share my learnings:

Set Up

Firstly, let’s use a simple example of two JavaScript classes:

Class Bar

class Bar {
	greetWorld( message ) {
		console.log( message );
	}
}

export default Bar;

Class Foo

We import bar and call the helloWorld function when we invoke greetWorld:

import Bar from './bar';

class Foo {
	constructor() {
		this.bar = new Bar();
	}

	helloWorld() {
		this.bar.greetWorld( 'Hello World' );
	}
}

export default Foo;

Initial attempt at testing helloWorld

We want to verify that the helloWorld function has called the greetWorld function in our Bar class. Here was my initial attempt (note: this doesn’t work):

import Foo from './foo';
import Bar from './bar';

describe( 'helloWorld', () => {
	it ( 'should call greetWorld', () => {
		const foo = new Foo();
		const bar = new Bar();

		jest.spyOn( bar, 'greetWorld' );

		foo.helloWorld();

		expect( bar.greetWorld ).toHaveBeenCalledTimes( 1 );
	});
});

Run this and you’ll get:

expect(jest.fn()).toHaveBeenCalledTimes(expected)

Expected number of calls: 1
Received number of calls: 0

The call from helloWorld to greetWorld hasn’t registered.

Correctly testing helloWorld

Looking in the Jest docs there are many ways to mock classes. I wanted a simple inline way of mocking it and here’s how I did it:

import Foo from './foo';
import Bar from './bar';

const mockGreetWorld = jest.fn();

jest.mock( './bar', () => {
	return jest.fn().mockImplementation(() => {
		return {
			greetWorld: mockGreetWorld
		};
	});
});

describe( 'helloWorld', () => {
	it ( 'should call greetWorld', () => {
		const foo = new Foo();
		const bar = new Bar();

		foo.helloWorld();

		expect( bar.greetWorld ).toHaveBeenCalledTimes( 1 );
	});
});

In this instance, we’ve called jest.mock with a module factory parameter. We cannot assign greetWorld: jest.fn(), it will fail:

// This will fail
jest.mock( './bar', () => {
	return jest.fn().mockImplementation(() => {
		return {
			greetWorld: jest.fn()
		};
	});
});

One of the limitations of using the module factory parameter is that jest.mock is hoisted to the top of the file so you cannot first define a variable and then use it in the factory. The only exception is made for variables that start with the word mock, so this will work:

// This will work
const mockGreetWorld = jest.fn();

jest.mock( './bar', () => {
	return jest.fn().mockImplementation(() => {
		return {
			greetWorld: mockGreetWorld
		};
	});
});

But this will fail:

// This will fail
const barGreetWorld = jest.fn();

jest.mock( './bar', () => {
	return jest.fn().mockImplementation(() => {
		return {
			greetWorld: barGreetWorld
		};
	});
});

You can read more about this way of mocking classes in the Jest documentation here.

A note on ES modules

Everything above assumes your code is being transformed to CommonJS before Jest runs it (the default if you’re using Babel or ts-jest), which is what allows jest.mock to be hoisted. If you’re running Jest with native ESM support enabled, jest.mock can’t be hoisted above your static imports, so this pattern won’t work as-is - you’ll need jest.unstable_mockModule and dynamic import() instead. For most setups though, the approach in this post works just the same today as it always has.