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
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
Leave a Reply