There's a concept called “mocking” that's very popular in testing. It's technically quite similar to stubbing, so the two are often confused (either intentionally or unintentionally). However, they serve different purposes and are used in different situations. Let's figure out what it is and when you need it.
Up until this point, we've viewed side effects as a hindrance to testing our logic. Stubs were used to isolate them, or the logic was specifically switched off in the test environment. After that, we were able to safely check the function worked properly.
Some situations require something else. Not the result of the function itself, but whether it performs the action we want, for example, does it send the right HTTP request with the right parameters? To do this, you'll need mocks. Mocks check how the code is executed.
HTTP
import nock from 'nock';
import { getPrivateForkNames } from '../src.js';
// Preventing random requests
nock.disableNetConnect();
test('getPrivateForkNames', async () => {
// Full domain name
const scope = nock('https://api.github.com')
// Full path
.get('/orgs/hexlet/repos/?private=true')
.reply(200, [{ fork: true, name: 'one' }, { fork: false, name: 'two' }]);
await getPrivateForkNames('hexlet');
// The `scope.isDone()` method returns `true` only when
// the corresponding query has been executed internally
expect(scope.isDone()).toBe(true);
});
This is called mocking. Mocks check that a given piece of code has been executed in a certain way. This can be a function call, an HTTP request, and so on. The job of the mock is to make sure that this happened, and find out how exactly it happened, for example, it can check that specific data was passed to the function.
What does this test do for us? In this case, not much. Yes, we're sure there was a call, but that in itself does not tell us anything. So when are mocks useful?
Imagine if we were developing the @octokit/rest library, the same library that makes requests to the GitHub API. The whole point of this library is to run the right queries with the right parameters.Therefore, it is necessary to check the execution of requests with the exact URLs. Only then can we be sure that it carries out the right requests.
This is the key difference between mocks and stubs. Stubs eliminate side effects, so as not to interfere with the verification of the result of the code, such as the return of data from a function. Mocks focus on exactly how the code works, what it does internally. In this purely technical way moc and stub are created the same way, except that the moc hangs waiting, checking calls. This leads to confusion because stubs are often referred to as mocks. There's nothing you can do about it, but try to understand for yourself what's being talked about. This is important, as the focus of the tests depends on it.
Functions
Mocks are quite often used with functions and methods. For example, they can check:
- That the function was called, and how many times it was called
- What arguments were passed to the function and how many
- What the function returned
Suppose we want to test the forEach
function. It calls a callback for each item in the collection:
[1, 2, 3].forEach((v) => console.log(v)); // or more simply [1, 2, 3].forEach(console.log)
This function doesn't return anything, so you can't test it directly. You can try to do it with mocks. Let's check that it calls the passed callback and passes the desired values to it.
Since we're looking at Jest, we'll use Jest's built in mechanism to create mocks. Other frameworks may have their own built-in mechanisms. In addition, as we've seen above, there are specialized libraries for mocks and stubs.
test('forEach', () => {
// Function mocks in Jest are created using the jest.fn function
// It returns a function that remembers all of its calls and the arguments passed
// This is then used for checks
const callback = jest.fn();
[1, 2, 3].forEach(callback);
// Now we check that it was called with the right arguments the right number of times
expect(callback.mock.calls).toHaveLength(3);
// the first argument of the first call
expect(callback.mock.calls[0][0]).toBe(1);
// the first argument of the second call
expect(callback.mock.calls[1][0]).toBe(2);
// The first argument of the third call
expect(callback.mock.calls[2][0]).toBe(3);
});
Using mocks, we were able to check that the function was called exactly three times, and a new element of the collection was consecutively passed to it for each call. In theory, we can say that this test really tests if forEach()
works. But can it be done in a simpler way, without using mocks and without binding it to internal behavior? It turns out you can. To do this, we just need to use closure:
test('forEach', () => {
const result = [];
const numbers = [1, 2, 3];
numbers.forEach((x) => result.push(x));
expect(result).toEqual(numbers);
});
Objects
Jest allows you to create mocks for objects as well. They're done with jest.fn()
, too, because it returns a constructor:
const myMock = jest.fn();
const a = new myMock();
const b = {};
const bound = myMock.bind(b);
bound();
console.log(myMock.mock.instances);
// > [ <a>, <b> ]
Through this moc you can find out any information about all the copies:
expect(someMockFunction.mock.instances[0].name).toEqual('test');
Advantages and disadvantages
Although there are situations in which mocks are necessary, in most situations they should be avoided. Mocks know too much about how the code works. Any test with mocks goes from a black box test to a white box one. The widespread use of mocks leads to two things:
- After refactoring, you have to rewrite the tests (and there's a lot of them!), even if the code works correctly. This happens because of how they're bound to how the code works.
- The code may stop working, but the tests pass because they focus not on its results, but on how it works internally.
Use real code wherever possible. If you can be sure that your code works without mocks, don't use mocks. Excessive mocking makes the tests useless and tricky to maintain. Black box tests are ideal.
Recommended materials
- Jest: Mocking functions
- Mock aren't stubs
- Nock: HTTP server mocking and expectations library for Node.js
- Mock Service Worker
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.