DEV Community

loading...
Cover image for How to mock an imported Typescript class with Jest
Code D' Ivoire

How to mock an imported Typescript class with Jest

devakone profile image Abou Kone ・3 min read

Sooner or later in your unit tests you will run into an issue where you need to import a class into your test and mock it, to keep up with good test hygiene. Jest offers a pretty good how to in their documentation on how to set it up for ES6 classes but if you try those instructions out of the box with Typescript, you will run into the type monster. This is a quick post to get it working for Typescript if you're using Jest. If you're an Angular developer and have not set up Jest yet, follow this great tutorial by Amadou Sall, the bonus is that you will also set up jest-preset-angular, which will help down the road.

SoundPlayer Class

Let's say this is your sound-player.ts file:


export class SoundPlayer {
  constructor() {
    this.foo = 'bar';
  }

  playSoundFile(fileName) {
    console.log('Playing sound file ' + fileName);
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice that this is not a default export. That's an important factor that if you follow the Jest documentation, their examples assumes that you're using default exports, which will matter later on in the mock.

SoundPlayer Mock

Now let's say you're writing a unit test for another class, let's say SoundPlayerConsumer and you want to mock SoundPlayer. If you don't have ts-jest installed, I highly recommend to add it to your Jest configuration now.

yarn add --dev ts-jest @types/jest
Enter fullscreen mode Exit fullscreen mode

Like I mentioned earlier, if you're using jest-preset-angular, it already comes "bundled" with ts-jest.

With ts-jest in the bag, mocking a Typescript class with Jest is as easy as:

import { mocked } from 'ts-jest/utils';
import { SoundPlayer } from './sound-player';

jest.mock('./sound-player', () => {
  return {
    SoundPlayer: jest.fn().mockImplementation(() => {
      return {
        playSoundFile: () => {},
      };
    })
  };
});

describe('SoundPlayerConsumer', () => {
  const MockedSoundPlayer = mocked(SoundPlayer, true);

  beforeEach(() => {
   // Clears the record of calls to the mock constructor function and its methods
   MockedSoundPlayer.mockClear();

  });

  it('We can check if the consumer called the class constructor', () => {
    const soundPlayerConsumer = new SoundPlayerConsumer();
    expect(MockedSoundPlayer).toHaveBeenCalledTimes(1);
  });

}

Enter fullscreen mode Exit fullscreen mode

It's pretty self explanatory but here are some clarification points:

  • Contrarily to the Jest documentation, since we're not using a default export, we have to reflect the namespace of the exported class module:
return {
    SoundPlayer: jest.fn().mockImplementation(() => {
      return {
        playSoundFile: () => {},
      };
    }
Enter fullscreen mode Exit fullscreen mode

If this was a default module, we could have written it simply as:

  return jest.fn().mockImplementation(() => {
    return {playSoundFile: mockPlaySoundFile};
  });
Enter fullscreen mode Exit fullscreen mode

If you're getting a “TypeError: ”X“.default is not a constructor.” when trying to run your tests, it's because you have not reflected the exported namespace properly.

  • The magic here happens because of the mocked method, which according to the documentation :

The mocked test helper provides typings on your mocked modules and even their deep methods, based on the typing of its source. It make use of the latest TypeScript features so you even have argument types completion in the IDE (as opposed to jest.MockInstance).

The first tell tale sign that your setup is not right would be getting an error of type error TS2339: Property 'mockClear' does not exist on type X X being the class you are trying to mock.

I hope this helps you write better unit tests.

Discussion (11)

pic
Editor guide
Collapse
komyg profile image
Felipe Armoni • Edited

Merci pour m'aider avec un test difficile d'Electron. Maintenant je peu faire un mock de l'object BrowserWindow et tester le classes de niveu plus bas. Il-y-a un example:

import { MyWindow } from './my-window';
import { mocked } from 'ts-jest/utils';
import { BrowserWindow } from 'electron';

jest.mock('electron', () => ({
  BrowserWindow: jest.fn(),
}));

describe('My Window Test', () => {
  it('should show the window', () => {
    const windowShow = jest.fn();

    const windowMock: any = mocked(BrowserWindow, true);
    windowMock.mockImplementation(() => ({
      show: windowShow,
      loadURL: jest.fn(),
    }));

    const myWindow = new MyWindow();
    myWindow.openWindow();
    expect(windowShow).toHaveBeenCalled();
  });
});
Enter fullscreen mode Exit fullscreen mode
Collapse
jakemonton profile image
Jake Monton

how do you call the playSoundFile method?

Collapse
soullivaneuh profile image
Sullivan SENECHAL

I try to mock a FeathersJS service class with your article example without any success.

I cannot figure out how to check class methods calls.

I ended up an another way with this:

import { mocked } from 'ts-jest/utils';
import app from '../../src/app';
import { Notifications } from '../../src/services/notifications/notifications.class';
import { NotificationsData } from '../../src/types';

jest.mock('../../src/services/notifications/notifications.class');
const mockNotificationsService = mocked(Notifications, true);
const mockNotificationsCreate = mocked(mockNotificationsService.mock.instances[0].create)
mockNotificationsCreate.mockImplementation(() => Promise.resolve({} as NotificationsData));

beforeEach(() => {
  // Clear registered calls from older tests.
  mockNotificationsCreate.mockClear();
})

describe('\'foo\' service', () => {
  it(
    'mock implement test',
    () => {
      return app.service('foo').create({ foo: 'bar' })
        .then(() => {
          expect(mockNotificationsCreate).toBeCalledTimes(1);
          expect(mockNotificationsCreate).toHaveBeenNthCalledWith(1, {
            applicationSlug: 'kidways',
            body: 'body',
            title: 'title',
            userIds: [
              'user1'
            ]
          }, {});
        })
    },
  );

  it(
    'mock implement test second',
    () => {
      return app.service('foo').create({ foo: 42 })
        .then(() => {
          expect(mockNotificationsCreate).toBeCalledTimes(2);
          expect(mockNotificationsCreate).toHaveBeenNthCalledWith(1, {
            applicationSlug: 'slug',
            body: 'body',
            title: 'title',
            userIds: [
              'user1'
            ]
          }, {});
          expect(mockNotificationsCreate).toHaveBeenNthCalledWith(2, {
            applicationSlug: 'slug',
            body: '42',
            title: 'title',
            userIds: [
              'user1'
            ]
          }, {});
        })
    },
  );
});
Enter fullscreen mode Exit fullscreen mode

This works but the implements looks wrong. I have to first mock the whole class with jest.mock then mock one of the method while storing it on a variable for test check.

Is it the way to go? Will you do the same way?

Thanks!

Collapse
dorianneto profile image
Dorian Neto

mocked() function from ts-jest saved my life! Thank you for the tip :)

Collapse
devakone profile image
Abou Kone Author

You're welcome!

Collapse
jotafeldmann profile image
Jota Feldmann

@devakone thanks a lot man! 🚀

Collapse
ultrarunner profile image
ultrarunner

Merci Beaucoup.

Collapse
devakone profile image
Collapse
dylajwright profile image
Dylan

Anyone looking for the link to the ts-jest mocked document, it is this kulshekhar.github.io/ts-jest/docs/...

Github and Kulshekhar have been inverted to find the docs.

Collapse
dylajwright profile image
Dylan • Edited

I also found value in understanding what the Mock Implementation is doing. In this link, jonathancreamer.com/testing-typesc..., explains what that is before mockImplementation was created. An assumption on creation regardless it explains that well which makes this article very fluent.

Collapse
devbirgit profile image
Birgit Pohl 🏡

Thanks. It looks pretty easy.
I was wondering when a mockedClass is re-used throughout the project, how this may look like. jest.mock() does not return anything, does it?