Prerequisites
Before you get started with this tutorial, I'm going to presume that you already have a JavaScript project that you're working on, and that you already understand some the absolute basics regarding testing and the reasons you might want to write tests. Sound vaguely familiar? Great, lets get started!
Why do we need to mock?
When writing unit tests its important to isolate the specific component (or unit) that we are testing at that specific time. If we don't do that effectively, then we can end up testing parts of our code outside of the piece that we want to test. To prevent this from happening, we can mock external parts of our code to simulate certain environments that our code may end up running in. By doing this we can ensure that our code always behaves as expected under different conditions.
Mocking with Jest
Fortunately for us, Jest makes it fairly simple to mock out different parts of your code (once you figure out how it's done). And I'll cover a few of the basic ways available to us now!
Setup
I'm going to presume that we have two classes. A 'ProductManager' which is the class which we are currently testing, and a 'ProductClient' which is to be used for fetching products from an API.
The ProductsClient may look something like the following.
export class ProductsClient {
async getById(id) {
const url = `http://localhost:3000/api/products/{id}`;
const response = await fetch(url);
return await response.json();
}
}
And the ProductManager could look something like this.
export class ProductManager {
async getProductToManage(id) {
const productsClient = new ProductsClient();
const productToManage = await productsClient.getById(id)
.catch(err => alert(err));
return productToManage;
}
}
So, the ProductManager fetches the product and returns its value, alerting us if there is an error while fetching. Seems simple enough right? Ok, let's see how we can unit test ProductManager by mocking the ProductsClient with Jest.
Writing the tests
The first method I'm going to show you uses Jest's automatic mocking. Simply import the module that you want to mock and call jest.mock(), like this.
import { ProductsClient } from './ProductsClient';
jest.mock('./ProductsClient');
Now, all of the methods on the ProductsClient class (i.e getById()) will automatically be mocked and return 'undefined'. Now this may be perfectly fine for a lot of uses. But there are a few issues with this for our case. Firstly, how can we test that ProductManager is returning the correct values if the ProductClient is just returning 'undefined' all the time? More importantly, however, if the call to getById() is returning 'undefined', our .catch() clause with throw an error, as we cannot call a method on 'undefined'!
Mocking our return value
So, how do we fix this issue? We mock the functions return value. Let's say our existing test looks like this.
it('should return the product', async () => {
const expectedProduct = {
id: 1,
name: 'football',
};
const productManager = new ProductManager();
const result = await productManager.getProductToManage(1); // Will throw error!
expect(result.name).toBe('football');
});
We need to make it so that the call to 'getById' on the ProductClient within the ProductManager class returns a promise which resolves to 'expectedProduct'. To do this, we need to assign a mock function to the ProductsClient's 'getById' method. However, as we are using ES6 class syntax, its not quite as simple as assigning it to 'ProductsClient.getById', we need to assign it to the object's prototype.
const mockGetById = jest.fn();
ProductsClient.prototype.getById = mockGetById;
Once we have done this, we can add what the mocked function should return.
const mockGetById = jest.fn();
ProductsClient.prototype.getById = mockGetById;
mockGetById.mockReturnValue(Promise.resolve(expectedProduct));
Now our completed test file should look like the following.
import { ProductsClient } from './ProductsClient';
import { ProductManager } from './ProductManager';
jest.mock('./ProductsClient');
it('should return the product', async () => {
const expectedProduct = {
id: 1,
name: 'football',
};
const productManager = new ProductManager();
const mockGetById = jest.fn();
ProductsClient.prototype.getById = mockGetById;
mockGetById.mockReturnValue(Promise.resolve(expectedProduct));
const result = await productManager.getProductToManage(1);
expect(result.name).toBe('football'); // It passes!
});
Conclusion
Hopefully this has served as a useful introduction to mocking class methods with Jest! If you enjoyed it I would love to hear your thoughts and suggestions for other things that you'd like to see from me. Thanks for reading!
Top comments (11)
Any hints on how to restore the original functionality at the end of the test? When using this technique I'm seeing that the order the tests run in now matters—each test after this one ends up using that same mock. :(
Looking at the mock function documentation - jestjs.io/docs/en/mock-function-api, you should be able to use
mockFn.mockClear()
,mockFn.mockReset()
, ormockFn.mockRestore()
depending on your needs. Hopefully this does what you need.I have tried all of these functions and still the value is either the same or undefined. Any hints?
Have you tried
afterEach(() => jest.resetAllMocks());
?If I change the getById method to
the test will still pass. This is not expected behavior I want from test
I'm not quite sure what you mean. In this example the 'ProductsManager' is the class being tested. The 'ProductsClient' is being mocked so we make assumptions about its behaviour in order to test the 'ProductsManager' in isolation. It's assumed that the 'ProductsClient' would be tested separately.
I'm not sure if directly modifying the prototype chain makes it impossible to reset/clear the mock.
An implementation a spying on the prototype chain seems to be working better for me i.e.
i get
ReferenceError: regeneratorRuntime is not defined
Very clever to mock the method through the
prototype
. I was struggling for hours, thank you!Is there a reason why using
and not .resolves?
thank you! I'm wondering why it's not written on the jest docs that we have to override the method through prototype.