Changing implementation of Jest mocks per test can be confusing. This blog post will present a simple solution for that. You'll see how each test can get its own mock for both constant values and functions. The solution doesn't rely on using require()
.
Sample app
Let's start with an example - we have a function - sayHello(name)
- it prints out Hi, ${name}
. And depending on configuration it either capitalizes the name or not.
say-hello.js
import { CAPITALIZE } from './config';
export const sayHello = (name) => {
let result = 'Hi, ';
if (CAPITALIZE) {
result += name[0].toUpperCase() + name.substring(1, name.length);
} else {
result += name;
}
return result;
};
And we want to test its behaviour like this:
say-hello.js
import { sayHello } from './say-hello';
describe('say-hello', () => {
test('Capitalizes name if config requires that', () => {
expect(sayHello('john')).toBe('Hi, John');
});
test('does not capitalize name if config does not require that', () => {
expect(sayHello('john')).toBe('Hi, john');
});
});
One of those tests is bound to fail. Which one - depends on the value of CAPITALIZE
.
Setting a value inside jest.mock()
will not help either. It will be the same as relying on the hardcoded value - one of the tests will fail.
jest.mock('./config', () => ({
CAPITALIZE: true // or false
}));
Changing mock of non-default const
So we need to change the mock of a non-default const
.
First, let's change the way we mock the config
module:
jest.mock('./config', () => ({
__esModule: true,
CAPITALIZE: null
}));
We do set CAPITALIZE
to null
, because we'll set its real value in the individual tests. We also have to specify __esModule: true
, so that we could correctly import the entire module with import * as config
.
Next step is we need to import the module:
import * as config from './config';
And finally change the mock value in each test:
import { sayHello } from './say-hello';
import * as config from './config';
jest.mock('./config', () => ({
__esModule: true,
CAPITALIZE: null
}));
describe('say-hello', () => {
test('Capitalizes name if config requires that', () => {
config.CAPITALIZE = true;
expect(sayHello('john')).toBe('Hi, John');
});
test('does not capitalize name if config does not require that', () => {
config.CAPITALIZE = false;
expect(sayHello('john')).toBe('Hi, john');
});
});
How does it work?
jest.mock()
replaces the entire module with a factory function we provide in its second argument. So when we import that module we get a mock instead of the real module. That also means that we can import the same module in the test itself. And that will give us access to the mock which behaviour we can change.
Why import entire module versus just the const
we need?
Why can't we just import in this way import CAPITALIZE from './config';
? If we import it in that way, we won't be able to re-assign a value to it. Values are always imported as constants.
TypeScript
If you're using TypeScript the line where you're changing the mock:
config.CAPITALIZE = true;
will give you an error:
Cannot assign to 'CAPITALIZE' because it is a read-only property
That's because TypeScript treats imports as constants and objects with read-only properties.
We can fix that by type casting to an object with writeable properties, e.g.:
import * as config from './config';
const mockConfig = config as { CAPITALIZE: boolean };
// and then in a test
mockConfig.CAPITALIZE = true;
Changing mock of export default const
Okay, but what if we need to change the mock of a value that is a default export of the module?
const CAPITALIZE = true;
export default CAPITALIZE;
We can use the same approach, we just need to mock the default
attribute:
import { sayHello } from './say-hello';
import * as config from './config';
jest.mock('./config', () => ({
__esModule: true,
default: null
}));
describe('say-hello', () => {
test('Capitalizes name if config requires that', () => {
config.default = true;
expect(sayHello('john')).toBe('Hi, John');
});
test('does not capitalize name if config does not require that', () => {
config.default = false;
expect(sayHello('john')).toBe('Hi, john');
});
});
TypeScript
As with mocking a constant that is non-default export, we need to type cast the imported module into an object with writeable properties
We can fix that by type casting to an object with writeable properties. This time though we change the default
attribute instead of CAPITALIZE
.
import * as config from './config';
const mockConfig = config as { default: boolean };
// and then in a test
mockConfig.default = true;
Changing mock of non-default function
What if the configuration is returned by a function instead of a constant:
const CAPITALIZE = true;
export default CAPITALIZE;
Actually, it'll be even more straightforward than dealing with constants, as we don't need to import the entire module via import * as entireModule
and as a result we won't have to provide __esModule: true
.
Our test will simply looks like this:
import { sayHello } from './say-hello';
import { shouldCapitalize } from './config';
jest.mock('./config', () => ({
shouldCapitalize: jest.fn()
}));
describe('say-hello', () => {
test('Capitalizes name if config requires that', () => {
shouldCapitalize.mockReturnValue(true);
expect(sayHello('john')).toBe('Hi, John');
});
test('does not capitalize name if config does not require that', () => {
shouldCapitalize.mockReturnValue(false);
expect(sayHello('john')).toBe('Hi, john');
});
});
TypeScript
This line
shouldCapitalize.mockReturnValue(false);
will give a TypeScript error of:
Property 'mockReturnValue' does not exist on type '() => boolean'.
Indeed, TypeScript thinks we've imported a function that returns a boolean
, not a Jest mock.
We can correct it again with type casting to a Jest mock.
import { shouldCapitalize } from './config';
const mockShouldCapitalize = shouldCapitalize as jest.Mock;
// and then in a test
mockConfig.default = true;
Changing mock of default function
There might also be a case that we want to change the behaviour of the function that is the default export of a module.
const shouldCapitalize = () => true;
export default shouldCapitalize;
In that case, we employ a technique similar mocking default constants - we'll mock `default`, set `__esModule: true` and will import the entire module with `*`.
import { sayHello } from './say-hello';
import * as config from './config';
jest.mock('./config', () => ({
__esModule: true,
default: jest.fn()
}));
describe('say-hello', () => {
test('Capitalizes name if config requires that', () => {
config.default.mockReturnValue(true);
expect(sayHello('john')).toBe('Hi, John');
});
test('does not capitalize name if config does not require that', () => {
config.default.mockReturnValue(false);
expect(sayHello('john')).toBe('Hi, john');
});
});
TypeScript
Similar to mocking a non default function, we need to type cast the imported module into an object with writeable properties
import * as config from './config';
const shouldCapitalizeMock = config.default as jest.Mock;
// and in a test
shouldCapitalizeMock.mockReturnValue(true);
Conclusion
All examples above rely on a simple premise that:
-
jest.mock()
mocks a specific module (unsurprisingly, huh?) - So everywhere you import it you'll get a mock instead of a real module
- And that applies to tests, as well
- So import mocked modules in test and change their implementation
Top comments (0)