Mocking is a very controversial concept in testing - some thought leaders advocate it as something "bad"😤, while others have to use mocks🙃 one way or another.
No matter what we think about it - mocks are around.
However, mocks can be different
- they could be
network mocks
(use msw) - or they could be
state mock
injected byjest.mock
or any other force (I would recommend magnetic-di)
But in all cases, there is a hill to die on - are you going to mock the entire universe 🛸 or just a little bit?
Let's start with the 🪐 entire universe
Jest.mock
jest.mock
(or vi.mock
) is a good example of "everything" - this helper mocks the entire module providing "empty stubs" for the original content.
Imagine a use case
// index.ts
import {getConfiguration} from './config';
export const isAdmin = () => getConfiguration().user.isAdmin;
// index.spec.ts
import {getConfiguration} from './config';
import {isAdmin} from './';
jest.mock('./config');
test('isAdmin', () => {
// ??? 🤷♂️
expect(isAdmin()).toBe(true);
});
To make this test run you need to specify the correct mock override for getConfiguration
. Can you do it?
Let's try
jest.mocked(getConfiguration).mockReturnValue({
// 50 different fields
user:{
// 20 other fields,
isAdmin: true
}
});
So you can, but you have to specify the "whole world", or typescript will not be happy.
You will not like it as well 🤮
What about defaults?
A better way to manage mocks is by providing defaults one can "extend" from
For example
import {defaultConfiguration} from './configuration-test-utils';
// ...
jest.mocked(getConfiguration).mockReturnValue(
merge({}, defaultConfiguration, {
user:{
isAdmin: true
}
);
That would greatly simplify life and establish an environment easier to maintain.
However it is still about mocking the whole world, while you need only one field 😭
Only one!
In out case we dont need to mock everything, it's more about TypeScript being too type-safe. Let's stop this!
jest.mocked(getConfiguration).mockReturnValue({
user:{
isAdmin: true
}
} as any/* 😘 */);
// test passes ✅
Well, as any
is not the best idea. Here is where DeepPartial
helps
Partial
is a TypeScript helper making keys of an object non-required.DeepPartial
is just a recursive helper for it. It has many implementations, here is one
This gives us the ability to write code like
jest.mocked(getConfiguration).mockReturnValue({
user:{
isAdmin: true
}
} as DeepPartial<ReturnType<typeof getConfiguration>>);
Yeah, the last line is 🤮 and one can improve it with utilities like shoehorn hiding all complexity underneath.
import { fromPartial } from "@total-typescript/shoehorn";
jest.mocked(getConfiguration).mockReturnValue(fromPartial({
user:{
isAdmin: true
}
}));
Better? Better!
....
However, what would happen if we change our code? You know - code always drifting somewhere...
export const isAdmin = () => (
getConfiguration().user.isAdmin ||
getConfiguration().user.isSuperAdmin
)
No tests will notice the difference, but our partial mock is no longer the correct one for our use case.
We need something better.
“actually better” is to refactor the code in a way you dont need to mock everything, but we are trying to complete the task without changing the game rules as not everybody can afford refactoring and not everybody want to make their test more testable for the sake or abstract testability (aka test induced design damage)
Something better
Let's change our test a little bit
// index.spec.ts
import {partialMock} from 'partial-mock'; // ⬅️⬅️
import {getConfiguration} from './config';
import {isAdmin} from './';
jest.mock('./config');
test('isAdmin', () => {
// ⬇️⬇️
jest.mocked(getConfiguration).mockReturnValue(partialMock({
user:{
isAdmin: true
}
});
expect(isAdmin()).toBe(true);
// but instead it will throw
});
Here we used partialMock
utility to apply DeepPartial
(but shoehorn
can do it), but also break test because the provided mock no longer represents the case.
- reproducible example - https://codesandbox.io/p/sandbox/partial-mock-example-8vr77s?file=%2Fsrc%2Findex.ts%3A21%2C20
What about "over mocking"?
Imagine the opposite situation - something complex becomes simpler, but your mocks are still too much.
For example, imagine we do define isSuperAdmin
missing in the example above, but we will no longer use it
const mock = partialMock<Data>({
isAdmin: true,
isSuperAdmin: true,
});
expectNoUnusedKeys(mock);
That's it, folks
That's it - partial mock helps with the "over-mocking", situations where you mock too much, and it also solves problems with the "under-mocking" where you missing some important pieces.
Pretty sure you never though about under-mocking. Until now.
Link to follow:
Top comments (0)