DEV Community

Jack Caldwell
Jack Caldwell

Posted on • Edited on

Mocking ES6 class methods with Jest!

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)

Collapse
 
cannikin profile image
Rob Cameron

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. :(

Collapse
 
jackcaldwell profile image
Jack Caldwell

Looking at the mock function documentation - jestjs.io/docs/en/mock-function-api, you should be able to use mockFn.mockClear(), mockFn.mockReset(), or mockFn.mockRestore() depending on your needs. Hopefully this does what you need.

Collapse
 
09wattry profile image
Ryan

I have tried all of these functions and still the value is either the same or undefined. Any hints?

Thread Thread
 
mattarau profile image
Matt Arau

Have you tried afterEach(() => jest.resetAllMocks()); ?

Collapse
 
amitozdeol profile image
Amitoz Deol

If I change the getById method to

async getById(id) {
    const url = `http://localhost:3000/api/user/{id}`;
    const response = await fetch(url);
    return await response.json();
  }

the test will still pass. This is not expected behavior I want from test

Collapse
 
jackcaldwell profile image
Jack Caldwell

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.

Collapse
 
09wattry profile image
Ryan • Edited

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.

import { ProductsClient } from './ProductsClient';
import { ProductManager } from './ProductManager';

it('should return the product', async () => {
  const expectedProduct = {
    id: 1,
    name: 'football',
  };

   jest.spyOn(ProductsClient.prototype, 'getById')
     .mockReturnValue(expectedProduct);

  const productManager = new ProductManager();
  const result = await productManager.getProductToManage(1); 

  expect(result.name).toBe('football'); // It passes!
});

Enter fullscreen mode Exit fullscreen mode
Collapse
 
muhammedmoussa profile image
Moussa

i get ReferenceError: regeneratorRuntime is not defined

Collapse
 
max profile image
Maxime Lafarie • Edited

Very clever to mock the method through the prototype. I was struggling for hours, thank you!

Collapse
 
jab3z profile image
jab3z

Is there a reason why using

mockGetById.mockReturnValue(Promise.resolve(expectedProduct))

and not .resolves?

Collapse
 
carloszan profile image
Carlos

thank you! I'm wondering why it's not written on the jest docs that we have to override the method through prototype.