36

I'm trying to mock an ES6 class with a constructor that receives parameters, and then mock different class functions on the class to continue with testing, using Jest.

Problem is I can't find any documents on how to approach this problem. I've already seen this post, but it doesn't resolve my problem, because the OP in fact didn't even need to mock the class! The other answer in that post also doesn't elaborate at all, doesn't point to any documentation online and will not lead to reproduceable knowledge, since it's just a block of code.

So say I have the following class:

//socket.js;

module.exports = class Socket extends EventEmitter {

    constructor(id, password) {
        super();

        this.id = id;
        this.password = password;

        this.state = constants.socket.INITIALIZING;
    }

    connect() {
        // Well this connects and so on...
    } 

};

//__tests__/socket.js

jest.mock('./../socket');
const Socket = require('./../socket');
const socket = new Socket(1, 'password');

expect(Socket).toHaveBeenCalledTimes(1);

socket.connect()
expect(Socket.mock.calls[0][1]).toBe(1);
expect(Socket.mock.calls[0][2]).toBe('password');

As obvious, the way I'm trying to mock Socket and the class function connect on it is wrong, but I can't find the right way to do so.

Please explain, in your answer, the logical steps you make to mock this and why each of them is necessary + provide external links to Jest official docs if possible!

Thanks for the help!

SpiXel
  • 4,338
  • 1
  • 29
  • 45
  • 1
    I wish I could upvote this more than once... I'm struggling to understand mocking of ES6 classes too: https://stackoverflow.com/questions/47402005/jest-mock-how-to-mock-es6-class-import – stone Nov 21 '17 at 22:35
  • Yeah, the docs are truly lacking in explaining this one, since I guess Jest is more oriented towards pre ES6 kinda javascript – SpiXel Nov 22 '17 at 05:54

2 Answers2

44

Update:

All this info and more has now been added to the Jest docs in a new guide, "ES6 Class Mocks."

Full disclosure: I wrote it. :-)


The key to mocking ES6 classes is knowing that an ES6 class is a function. Therefore, the mock must also be a function.

  1. Call jest.mock('./mocked-class.js');, and also import './mocked-class.js'.
  2. For any class methods you want to track calls to, create a variable that points to a mock function, like this: const mockedMethod = jest.fn();. Use those in the next step.
  3. Call MockedClass.mockImplementation(). Pass in an arrow function that returns an object containing any mocked methods, each set to its own mock function (created in step 2).
  4. The same thing can be done using manual mocks (__mocks__ folder) to mock ES6 classes. In this case, the exported mock is created by calling jest.fn().mockImplementation(), with the same argument described in (3) above. This creates a mock function. In this case, you'll also need to export any mocked methods you want to spy on.
  5. The same thing can be done by calling jest.mock('mocked-class.js', factoryFunction), where factoryFunction is again the same argument passed in 3 and 4 above.

An example is worth a thousand words, so here's the code. Also, there's a repo demonstrating all of this, here: https://github.com/jonathan-stone/jest-es6-classes-demo/tree/mocks-working

First, for your code

if you were to add the following setup code, your tests should pass:

const connectMock = jest.fn(); // Lets you check if `connect()` was called, if you want

Socket.mockImplementation(() => {
    return {
      connect: connectMock
    };
  });

(Note, in your code: Socket.mock.calls[0][1] should be [0][0], and [0][2] should be [0][1]. )

Next, a contrived example

with some explanation inline.

mocked-class.js. Note, this code is never called during the test.

export default class MockedClass {
  constructor() {
    console.log('Constructed');
  }

  mockedMethod() {
    console.log('Called mockedMethod');
  }
}

mocked-class-consumer.js. This class creates an object using the mocked class. We want it to create a mocked version instead of the real thing.

import MockedClass from './mocked-class';

export default class MockedClassConsumer {
  constructor() {
    this.mockedClassInstance = new MockedClass('yo');
    this.mockedClassInstance.mockedMethod('bro');
  }
}

mocked-class-consumer.test.js - the test:

import MockedClassConsumer from './mocked-class-consumer';
import MockedClass from './mocked-class';

jest.mock('./mocked-class'); // Mocks the function that creates the class; replaces it with a function that returns undefined.

// console.log(MockedClass()); // logs 'undefined'

let mockedClassConsumer;
const mockedMethodImpl = jest.fn();

beforeAll(() => {
  MockedClass.mockImplementation(() => {
    // Replace the class-creation method with this mock version.
    return {
      mockedMethod: mockedMethodImpl // Populate the method with a reference to a mock created with jest.fn().
    };
  });
});

beforeEach(() => {
  MockedClass.mockClear();
  mockedMethodImpl.mockClear();
});

it('The MockedClassConsumer instance can be created', () => {
  const mockedClassConsumer = new MockedClassConsumer();
  // console.log(MockedClass()); // logs a jest-created object with a mockedMethod: property, because the mockImplementation has been set now.
  expect(mockedClassConsumer).toBeTruthy();
});

it('We can check if the consumer called the class constructor', () => {
  expect(MockedClass).not.toHaveBeenCalled(); // Ensure our mockClear() is clearing out previous calls to the constructor
  const mockedClassConsumer = new MockedClassConsumer();
  expect(MockedClass).toHaveBeenCalled(); // Constructor has been called
  expect(MockedClass.mock.calls[0][0]).toEqual('yo'); // ... with the string 'yo'
});

it('We can check if the consumer called a method on the class instance', () => {
  const mockedClassConsumer = new MockedClassConsumer();
  expect(mockedMethodImpl).toHaveBeenCalledWith('bro'); 
// Checking for method call using the stored reference to the mock function
// It would be nice if there were a way to do this directly from MockedClass.mock
});
stone
  • 8,422
  • 5
  • 54
  • 66
  • Thanks for your answer, well, it actually solves my problem with a little bit of insight into how things work. A problem though, I should put that **Socket.mockImplementation** inside every test I write (test("it ...")), or otherwise, the counter on the global mock method will only increase (the first test would check for .calls.length to be 1, the next should check for it being 2 and so on ...). How would you solve that ? I tried running *Socket.mockImplementation* in a **beforeEach** func, but no luck ! – SpiXel Nov 22 '17 at 10:43
  • Call `Socket.mock.mockClear()` in `beforeEach()`. http://facebook.github.io/jest/docs/en/mock-function-api.html#mockfnmockclear – stone Nov 22 '17 at 10:49
  • 1
    Yeah, see it now, though `Socket.mockClear()` was what worked. I'm accepting your answer as the right answer until maybe a better one comes up. Many thanks ! – SpiXel Nov 22 '17 at 11:01
  • There's a complete explanation for doing this with `jest.mock('./path', factoryFunction)` that applies to this question. https://stackoverflow.com/questions/47402005/jest-mock-how-to-mock-es6-class-default-import-using-factory-parameter/47502477#47502477. The strategy described there also works with manual mocks, but that has not been written up anywhere yet. I hope to update this answer at some point. – stone Jan 08 '18 at 07:53
  • 1
    Demo repo showing mocking ES6 classes three different ways: using manual mock in \__mocks\__ folder; factory function; and manual mock plus `mockImplementation()`: https://github.com/jonathan-stone/jest-es6-classes-demo/tree/mocks-working – stone Jan 10 '18 at 01:49
  • This might be a long shot but what i have found is that my function is unable to detect the mocked methods being called. The constructor it sees properly and verifies it's calls. But the mocked methods on the class keep saying it wasn't called, and even console logging the mock has a different signature of `mockConstructor` – Byrd Sep 10 '18 at 15:20
  • @Byrd, I suggest you create a new question. It's hard to help if we can't see your code. – stone Sep 10 '18 at 21:07
  • I did https://stackoverflow.com/questions/52263000/es6-class-jest-mocking/52263159#52263159, Thanks in advance – Byrd Sep 10 '18 at 21:59
  • @stone Would you mind looking at [this question](https://stackoverflow.com/questions/55522669/jest-mocking-of-classes-with-di-dependencies), which is probably a good idea to have in your official Jest docs? – lonix Apr 04 '19 at 18:32
  • @Frondor it's open source, you're welcome to improve it! – stone May 02 '19 at 04:56
  • Is there any difference to typescript or has something changed? Trying the 'sound-player-consumer-factory-mock' and I get a 'mockClear' does not exist error. No surprise beacuse mockClear does not exist in this class. – aProgger Dec 05 '21 at 10:08
  • 1
    Found it out myself. Sometimes one has to ask a question just to find a hint on what to look for. :) I had to tell typescript my class is a Mock now. So after 'jest.mock('./MyClass');' I do 'const myClass = MyClass as unknown as jest.Mock;' TS compiler is happy. – aProgger Dec 05 '21 at 10:25
  • `Property 'mockImplementation' does not exist on type ...` – leonheess Oct 24 '22 at 15:13
  • @leonheess if that's a Typescript error, see aProgger's comments above - you have to tell Typescript that the type is different from what it's expecting. If it's a runtime error, then a link to an example repo/gist/etc that generates the error would be helpful. – stone Oct 24 '22 at 21:08
0

For me this kind of Replacing Real Class with mocked one worked.

// Content of real.test.ts

jest.mock("../RealClass", () => {
  const mockedModule = jest.requireActual(
    "../test/__mocks__/RealClass"
  );
  return {
    ...mockedModule,
  };
});

var codeTest = require("../real");
  it("test-real", async () => {
    let result = await codeTest.handler();
    expect(result).toMatch(/mocked.thing/);
  });

// Content of real.ts
import {RealClass} from "../RealClass";
export const handler = {
   let rc = new RealClass({doing:'something'});
   return rc.realMethod("myWord");
}
// Content of ../RealClass.ts
export class RealClass {
  constructor(something: string) {}
  async realMethod(input:string) {
    return "The.real.deal "+input;
  }
// Content of ../test/__mocks__/RealClass.ts
export class RealClass {
  constructor(something: string) {}
  async realMethod(input:string) {
    return "mocked.thing "+input;
  }

Sorry if I misspelled something, but I'm writing it on the fly.

vencedor
  • 663
  • 7
  • 9
  • 1
    There is no need to use the ModuleFactory syntax here. Jest will automatically replace RealClass with __mocks__/RealClass. See: https://jestjs.io/docs/en/es6-class-mocks.html#automatic-mock – 99linesofcode Jan 27 '21 at 13:32