I have a test I'm trying to run and I want to mock a module using jest.mock. However, I can't seem to get this one to work, in the example below, the original serializeProduct
method is always invoked. I've tried spies, I've tried mocking the module. When I log out the variables that are the mocks/spies they are indeed mock functions at least so the mocking is happening. It's just that something is causing it to invoke that original method instead of the mock.
product.serializer.ts
import { ProductDto } from './product.dto';
export function serializeProducts(value: any[]): ProductDto[] {
return value.map(serializeProduct);
}
export function serializeProduct(value: any) {
return value;
}
product.serializer.spect.ts in the same folder as the above.
import { expect } from '@jest/globals';
import * as productsSerializer from './products.serializer';
describe('serializer', () => {
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
describe('serializeProducts method', () => {
it('should foo', () => {
const packages = [1,2,3,];
const spy = jest.spyOn(productsSerializer, 'serializeProduct').mockImplementation(() => undefined);
productsSerializer.serializeProducts(packages);
expect(spy).toHaveBeenCalledTimes(3);
});
});
});
I have also tried module mocking originally like so
jest.mock('./products.serializer', () => ({
...jest.requireActual('./products.serializer'),
serializeProduct: jest.fn()
});
import { expect } from '@jest/globals';
import * as productsSerializer from './products.serializer';
describe('serializer', () => {
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
describe('serializeProducts method', () => {
it('should foo', () => {
const packages = [1,2,3,];
(productsSerializer.serializeProduct as jest.Mock).mockImplementation(() => undefined);
productsSerializer.serializeProducts(packages);
expect(productsSerializer.serializeProduct).toHaveBeenCalledTimes(3);
});
});
});
I've also individually imported the methods in the above example instead of importing * as
to no avail.
I've got examples of this working in the very same project I'm working on and yet this one doesn't.
EDIT The accepted solution on the suggested other question is not viable as it requires changing application code to fix a test.
The other highest solution is to move exports around - also changing application code. I have confirmed that if I move one of the methods out it does work, but this really shouldn't be a viable solution either. It might work for some, but it doesn't make sense in the context of my application to do that.
So I would like to find out just why this does not work. If it should work then there should be a solution that doesn't involve changing application code.
EDIT 2 I have found that turning the methods into exported constants and defining them as arrow functions fixes the issue. Which means it probably has something to do with jest/node imports.
You're having trouble mocking serializeProduct
because serializeProducts
is calling it from within the same module. The root cause of this issue is how the code is transpiled.
Let's take a look at the following example:
export function foo() {};
export function bar() { foo() };
Your transpiler would most likely transform it to something like this (simplified):
function foo() {};
function bar() { foo() };
exports.foo = foo;
exports.bar = bar;
The issue here is that we're mocking (reassigning) exports.foo
in our tests, while bar
is calling the original foo
.
One solution
So, one thing we can do is call the exported foo
property, aka, exports.foo
, so that when it's mocked, we call the mock instead.
export function foo() {};
export function bar() { exports.foo() };
This is valid code and will be correctly transpiled, but it's ugly.
A better solution for you
Since you're using Typescript, then there's a good chance you're using ts-jest
as your transformer. ts-jest
has a nice little quirk where it replaces exported variables with their exports
member counterparts.
So instead of calling a function we define using a function statement, we just need to call an exported variable that's assigned to the function we want, and that variable will be mockable because of ts-jest
's helpful substitutions. We can do this in different ways:
function foo () {};
// All 3 of these are replaced by `exports.<name>` by `ts-jest` where they're called
export const foo1 = foo; // Reassign to a new variable
export const foo2 = function() {} // Anonymous function
export const foo3 = () => {} // Arrow function
Here's a quick illustration I got by debugging a test I ran locally. Let's see the original module, and how it was transformed by ts-jest
.
// Original
export function foo() {};
export const bar = function() {}
export function buzz() {
foo();
bar();
}
// Transformed
Object.defineProperty(exports, "__esModule", { value: true });
exports.bar = void 0;
exports.foo = foo;
exports.buzz = buzz;
function foo() {};
const bar = () => {};
exports.bar = bar;
function buzz() {
foo();
(0, exports.bar)();
}
Notice in buzz
how bar
's function call was replaced by its exports
counterpart. Now we can use spyOn
with no issues.
More reading: A github issue about this exact situation.
Honorable mention - Dependency Injection
For your particular case, serializeProducts
is essentially a wrapper around Array.map
which makes it a prime candidate for some dependency injection. You could give it a second parameter, pass it serializeProduct
by default, and then pass it a mock function in your tests.
It gives serializeProducts
an added degree of flexibility without changing the overall workflow too much, and makes testing a lot easier.