Mocking functions in Jest for Unit Testing with Typescript
Jest is a test framework for javascript/typescript that allows testing to be done on any functions that are created and aids in writing code with single responsibility and other good practises as it is required for tests to run and for tests to be written effectively.
This article will look into jest.spyOn() and will aim to help new users of jest understand properly how to create mock implementations and how code should be structured to ensure that created functions can be mocked effectively.
Let’s start with a basic example of mocking a function using typescript and jest:
mockable.ts
const mockable = () => {
console.log('You can mock me');
};
export default {
mockable,
};
index.ts
import mockableDefaults from './mockable';
const main = () => {
mockableDefaults.mockable();
};
export default {
main,
};
This function is mockable and this can be done and testing using a simple test like this:
import {jest, test, expect} from '@jest/globals';
import * as index from '../src/index';
import mockableDefaults from '../src/mockable';
test('Mockable can be mocked', () => {
const logSpy = jest.spyOn(console, 'log');
jest.spyOn(mockableDefaults, 'mockable').mockImplementation(() => {
console.log('mockable has been mocked');
});
index.default.main();
expect(logSpy).nthCalledWith(1, 'mockable has been mocked');
});
This test uses jest.spyOn() to look at 2 different functions, first we spy on console.log() so that later we can check what this function was called with. Secondly we spy on the default exports from the mockable.ts file and check if the mockable function has been called and replaces any calls to it with our mock implementation here saying that mock has been mocked.
We then call main from our index.ts file and check that our console.log from our mock implementation was called by using expect to see if the first time console.log was called it was called with our expected string.
Moving onto an example of a scenario where mocking will not work:
If we add a function called unmockable into our index.ts file that looks like this:
export const unmockable = () => {
console.log("You can't mock me");
};
We then need to call this function in our main method and that it is added into our default exports:
const main = () => {
mockableDefaults.mockable();
unmockable();
};
export default {
main,
unmockable,
};
We can then write a simple test for this function and try to mock it:
test('Unmockable cannot be mocked', () => {
const logSpy = jest.spyOn(console, 'log');
jest.spyOn(index.default, 'unmockable').mockImplementation(() => {
console.log('unmockable has been mocked');
});
index.default.main();
expect(logSpy).nthCalledWith(2, "You can't mock me");
});
Here we again spy on console.log to check what it was called with and this time we try to mock unmockable, and we check to see with our expect that it has not been mocked and this test passes? This will be explained later after the next test.
Mocking the unmockable:
If we add a third line into our main function, so it looks like this:
const main = () => {
mockableDefaults.mockable();
unmockable();
exports.unmockable();
};
And we update our unmockable function to be an export of index.ts:
export const unmockable = () => {
console.log("You can't mock me");
};
And we try another test to mock unmockable:
test('exports.unmockable can be mocked', () => {
const logSpy = jest.spyOn(console, 'log');
jest.spyOn(index, 'unmockable').mockImplementation(() => {
console.log('unmockable has been mocked');
});
index.default.main();
expect(logSpy).nthCalledWith(3, 'unmockable has been mocked');
});
In this test we again spy on and expect unmockable to be successfully mocked this time and it is? What changed?
This test failure happened to me many times before I figured out why it occurred, this situation where functions cannot be mocked appears when functions lie in the same file as where they are called as when you are mocking functions you are looking at the imported version of this function and when a function lies within the same file as where it is called it is using a local version of the function rather than the exported version. This is why when calling unmockable using “exports.unmockable” the mocking works as it is referencing the exported version of the function rather than the local.