Mock/Spy exported functions within a single module in Jest

A brief guide on how to test that a function depends on another function exported by the same module

Davide Rama
Nov 12, 2018 · 3 min read
Image for post
Image for post

The Problem

You have a module that exports multiple functions. One of these functions depends on another function of the same module.

export function foo () { ... }
export function bar () { foo() }

You want to assert that when executing bar() , it will also fire the execution of foo().

This would seem to be a classic situation for using Jest functionalities spyOn or mock. Therefore, you would expect to be able to write a test something like this:

import * as myModule from './myModule';test('calls myModule.foo', () => {
const fooSpy = jest.spyOn(myModule, 'foo');
myModule.bar();expect(fooSpy).toHaveBeenCalledTimes(1);
});

Surprisingly or not, this test would fail with the message Expected mock function to have been called one time, but it was called zero times.:

Image for post
Image for post

You could try using jest.mock() or any other Jest interface to assert that your bar method depends on your foo method.

You will end up blaming Jest for causing the error and regretting the moment you decided to start writing your tests with it.

The Reason

In more detail, it is because of how Javascript is compiled by babel. This is the output of myModule once compiled:

var foo = function foo() {};
var bar = function bar() { foo(); };
exports.foo = foo;
exports.bar = bar;

When the function bar is declared, the reference to the foo function is enclosed with the function declaration.

In your test environment, when you import foo and bar what you are really importing is exports.foo and exports.bar.

Hence, when you mock foo what you are really mocking is exports.foo.

When executing bar(), what bar invokes is its enclosed reference of foo.

Therefore, the test correctly fails since exports.foo is never called when executing bar()!

The Solution(s)

While investigating on the internet you might find some solutions to overcome this “issue” adopting the usage of the require function.

Now, just to be precise, the require function is not part of the standard JavaScript API. It is a built-in function of the Node.js environment with the purpose of loading modules.

If you, like me, find this solution undesirable, there are two ways in which you could restructure your code and be able to test that one of the functions depends on the other.

A single source of truth

The first strategy you could use is storing the references to your methods in an object which you will then export. bar will invoke the reference of foo stored in that object.

var foo = function foo() {};
var bar = function bar() { exportFunctions.foo(); };
const exportFunctions = {
foo,
bar
};
export default exportFunctions;

In this way, you will import and mocking the same reference to foo which is called by bar() and the same test previously defined will now pass!

Separation of concerns

On the other hand, you can separate the concerns of your code and declare the two functions in two different modules.

// fooModule.js
export function foo () { ... }
// barModule.js
export function bar () { fooModule.foo() }

This will result in a standard external module dependency scenario.

You can now adapt your test to be:

import foo from './fooModule';
import bar from './barModule';
test('calls fooModule.foo', () => {
const fooSpy = jest.spyOn(fooModule, 'foo');
bar();expect(fooSpy).toHaveBeenCalledTimes(1);
});

I hope you will find this article helpful on your way to happy, clean code delivery!

Thank you to my colleagues Sasha and Brett aka Je(s)tt for the support and the enjoyable time spent together while investigating on this topic!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store