How to Mock a TypeScript class or dependency using Jest

Jest is an awesome and easy to use testing framework for JavaScript, but when it comes to TypeScript and mocking (specially to mock typescript classes) it can really become painful as TypeScript is not relaxed about the types as JavaScript is. In this article I am going to show you how to mock a dependency class and its functions. There are lots of treads in stack overflow, but unfortunately most of them are not really useful. Here I am showing you what you really need to do.

We are going to take the same scenario as jest documentation for ES6 Class Mocks. Though, jest documentation obviously is not going to work for TypeScript, otherwise we would not have this discussion.

I trust you got your TypeScript/Jest environment right. Otherwise you are going to get errors about import, types and etc. even before we start. If you want to get your environment right, you might want to read How to setup a TypeScript project dev environments like a pro first.

Scenario : Dependency Class testing with Jest in TypeScript

Get the code from GitHub

We have a typescript class called SoundPlayer and it has a function called playSoundFile that expects a string argument. As a part of our test we want this faction to be called exactly one time and with a pre determined argument. Note that we are not testing SoundPlayer (I am telling you more shortly)

//file: sound-player.ts
export default class SoundPlayer {
    foo: string;
    constructor() {
        this.foo = 'bar';
    }

    playSoundFile(fileName: string) {
        console.log('Playing sound file ' + fileName);
    }
}

We have another class, namely SoundPlayerConsumer that uses SoundPlayer internally to play a song. This is the class we are interested in testing

//file : sound-player-consumer.ts
import SoundPlayer from './sound-player';

export default class SoundPlayerConsumer {
    soundPlayer: SoundPlayer;
    constructor() {
        this.soundPlayer = new SoundPlayer();
    }

    playSomethingCool() {
        const coolSoundFileName = 'song.mp3';
        this.soundPlayer.playSoundFile(coolSoundFileName);
    }
}

If we want to use this SoundPlayerConsumer in a nodeJs program -forget about jest and testing for a second, we need to new up SoundPlayerConsumer and call playSomethingCool from the instance . Let’s do it in index.ts

// file index.ts
import SoundPlayerConsumer from "./sound-player-consumer";

const spc = new SoundPlayerConsumer();
spc.playSomethingCool();

Then when we run it using node (there are several way to run typescript project, I use nodemon for development) you should get a console log that says :

Playing sound file song.mp3

Note that the console.log is within soundPlayer, so SoundPlayerConsumer internally creates instance of soundPlayer and calls soundPlayer.playSoundFile with some argument and that is how we get the console log.

What and why are we Mocking

It is important to clarify what are we testing, what we are mocking and why we are mocking that. Although, this mocking discussion is not specific to Jest and TypeScript, it is very applicable to our example. We are neither testing the index and nor SoundPlayer. We are specifically are testing SoundPlayerConsumer. At this test we are trusting that SoundPlayer is going to do its job, because either we wrote some unit tests for that class too, or it is a third party library that we don’t maintain the code, but we trust it.

What we are testing is that SoundPlayerConsumer is going to do its job by doing all its internal calculations and as result it uses the SoundPlayer class correctly.

It is very important to understand what to mock while you are writing your tests; you should never mock the class you are actually testing (in our case SoundPlayerConsumer ), but you should mock classes and imported functions that the class under test (in our case SoundPlayerConsumer ) internally calls.

Test case and mock typescript class


//file: sound-player-consumer.test.ts
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); 
const mockSoundPlayer = jest.mocked(SoundPlayer, true);

beforeEach(() => {
    mockSoundPlayer.mockClear();
});

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

it('checks if the consumer calls a method of the class instance', () => {
    // Show that mockClear() is working:
    expect(SoundPlayer).not.toHaveBeenCalled();

    const soundPlayerConsumer = new SoundPlayerConsumer();
    // mocked class constructor should have been called now:
    expect(SoundPlayer).toHaveBeenCalledTimes(1);

    const coolSoundFileName = 'song.mp3';
    soundPlayerConsumer.playSomethingCool();

    const mockSoundPlayerInstance = mockSoundPlayer.mock.instances[0];
    const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile as jest.Mock;
    expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
    // Equivalent to above check:
    expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
    expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});

Lets go trough the important lines of the sample test file:

line 5: you say to jest that you want to mock typescript class SoundPlayer and therefore a mock constructor is going to run instead of the real SoundPlayer. (this is basically identical to JavaScript)

line 6: you get the first jest/typescript specific line; because you need the mock type for your test, you define a mockSoundPlayer and buy giving the second argument true you say you want to deeply mock SoundPlayer class.

line 21: you new up the typescript class under the test (soundPlayerConsumer)

line 26: you call a function of the class you are testing. Note that we are not mocking class under the test or its functions. This function internally calls SoundPlayer.playSoundFile(fileName : string) and what we want to make sure is that playSoundFile is called with right file name. This means that soundPlayerConsumer is doing its job correctly.

line 29: you reference mocked instance of PlaySoundFile function. This is the most unclear thing about the mocking an internal function of a dependency class in typescript. The key is “as jest.Mock” at the end of this line.

line 30: you successfully can test that playSoundFile is being called and the argument with value of ‘song.mp3’ is passed to it, thus the soundPlayerConsumer is doing its job correctly and we are good!

Next please read about How to mock class constructor with parameters- Jest and TypeScript

Get the code from GitHub

More Good Reads ?


Posted

in

by

Comments

4 responses to “How to Mock a TypeScript class or dependency using Jest”

  1. Ramesh Avatar
    Ramesh

    How about mocking/stubbing typescript interfaces?
    I have a complex interface that is consists of properties, other interfaces, and some async functions ex:


    interface IFoo{
    someProp: string
    bar : IBar
    buz: ()=>Promise
    }

    Then I have a function that gets IFoo as an argument. How to I mock that interface?

  2. Daniel Abrahamberg Avatar

    It is a good question Ramesh. In many cases while tesing and face a complex interface, the function that asks for argument of complex type is not really using the whole object but only some of properties of that (due to mocking of functions).

    In case of your example let’s say we have fooUser(IFoo) and we know that it just uses someProp and buz.

    function fooUser(foo: IFoo) {
    return `${foo.someProp} and ${foo.buz().buzPop2}`
    }

    Then I would create a javascript object as IFoo with only props my function really needs and pass it to the function:


    const foo = {
    someProp: 'some value' ,
    buz: ()=> {buzPop2:"prop 2"}
    // note that we ignore bar and any other prop fooUser does not use.
    } as IFoo

    // now I have a valid I typescript foo with only those values I need
    fooUser(foo);

    Hope it solves your problem 🙂

  3. Patryk Avatar
    Patryk

    It doesn’t work:

    `mockClient.mock.instances;`

    is always empty

    1. Daniel Abrahamberg Avatar

      Hello Patryk!
      It of course works! You can test it yourself, I put the code in GitHub just for you! check it out 🙂

      Did you miss the line 5?

      jest.mock('./sound-player');

      In that case the only way get the mockSoundPlayer.mock.instances empty is to miss to crate an instance of it.
      In our example you can miss to new up SoundPlayerConsumer() during the test or miss to create and instance of SoundPlayer in the contractor of SoundPlayerConsumer
      Otherwise you should get this when you console.log(mockSoundPlayer.mock.instances)

      [
      SoundPlayer {
      playSoundFile: [Function: playSoundFile] {
      _isMockFunction: true,
      getMockImplementation: [Function (anonymous)],
      mock: [Getter/Setter],
      ...
      }
      ]

Leave a Reply

Your email address will not be published. Required fields are marked *