During the earlier
lesson
, we learned the stubs
and realized what they are used for. Now is the time for mocks
, which are used very often during unit testing.Mocks definition
Mocks are used to replace the implementation of a specific module. Their main objective is to provide:
Why are mocks so important?
Think of a situation where you make a query to a mailing service in the code that you are testing. If you don't mock the API of that service then you're making real queries.
It would cost you a bit 💰.
Why do we need to use mocks?
Let's say we have the following function that is designed to build a path for specific article:
import type { Lang } from 'apps/blog/models';
import path from 'path';
export const createArticlePath = (lang: Lang, ...rest: string[]): string =>
path.join(
process.cwd(),
'apps',
'blog',
'content',
lang,
'articles',
...rest
);
What catches the eye immediately is the use of the
path
module and the globally available process
variable.The path module is not a part of our application, but an external library - we assume that it works properly, we don't want to test it.
The process variable is an object created by the
NodeJS
runtime environment. It has properties and methods. Also, we have to assume that it works correctly and we don't want to test it.How should we test it? We need to mock both and simulate the behavior of the
cmd()
and join()
methods. We know how they work so it won't be a problem.Let's mock
This is how the
join()
and cwd()
methods work:
path.join('/path/to', 'file.txt');
// Output (on Unix-like systems): /path/to/file.txt
// Output (on Windows): \path\to\file.txt
process.cwd()
// Returns current working director - "/home/user/project"
Now all we need to do is create a test in which we substitute their implementation and check whether our own function works as expected.
// This import is essential.
import path from 'path';
// This we will test.
import { createArticlePath } from '.';
const basePath = 'C:\Users\pracapraca\Dream-stack-for-React-dev\system';
// We replace the implementation of the path module.
jest.mock('path', () => ({
join: jest.fn(), // Allows methods from "jest" be used.
}));
describe('Path for dedicated article is created when', () => {
// Temp variable.
const originalCwd = process.cwd;
beforeAll(() => {
// We replace the cwd method implementation.
Object.defineProperty(process, 'cwd', {
value: () => basePath,
writable: true,
});
});
afterAll(() => {
// We restore the original implementation.
process.cwd = originalCwd;
});
it('absolute path is composed', () => {
// We replace the implementation.
(path.join as jest.Mock).mockImplementationOnce((...args: string[]) =>
args.join('\')
);
const result = createArticlePath('en', 'my-file.mdx');
// We check that the path is correct.
expect(result).toBe(
basePath + '\apps\blog\content\en\articles\my-file.mdx'
);
});
});
In the first mock we replace the implementation of the path module and the join method. Thanks to the
jest.fn()
we will be able to change the implementation in any other test as needed.
// Here we mock module.
jest.mock('path', () => ({
join: jest.fn(), // This allows us to change the implementation later.
}));
// Here we substitute implementations for a specific test.
(path.join as jest.Mock).mockImplementationOnce((...args: string[]) =>
args.join('')
);
In the second one, we used
defineProperty
to override a globally available process object property.
// Temp variable.
const originalCwd = process.cwd;
// The code in the callback will execute before first test execution.
beforeAll(() => {
// We replace the cwd method implementation.
Object.defineProperty(process, 'cwd', {
value: () => basePath,
writable: true // Allows to change process.cwd later.
});
});
// The code in the callback will execute after all tests are completed.
afterAll(() => { // It's executed after all tests
// We restore the original implementation.
process.cwd = originalCwd; // This is allowed thanks to writeable: true
});
It is very important to assign a starting value to global objects after testing. If we don't do this, we expose ourselves to side effects and the fact that tests in this file can affect other tests -
which is very bad!
Note the use of the
object descriptor (writeable: true)
, which allowed us to restore to the initial value later. Without this change, it is not possible to overwrite the cwd
property implementation.Bravo! Now you know how important mocks are and how to create them for modules, methods and global objects. You have to admit that their use is much wider than in the case of stubs.
If you enjoyed it, be sure to visit us on
Linkedin
where we regularly upload content from programming.- 1. Basics
10 minutes
Software testing
2 m
Grouping the tests
1 minute
The usage of describe and it
2 m
The best practices for naming tests
2 m
Types of tests
3 m
- 2. Mastering unit testing
31 minutes