Categories
Testing

Creating Automatic Jest Mocks

Spread the love

To create unit tests, we often have to mock functions to bypass various parts of the code. For example, if we don’t want to run code that calls an API in a function, then we have to create a mock of that function.

In this article, we’ll look at some ways to create mocks functions and classes in Jest tests.

ES6 Class Mocks

We can use Jest to create ES6 class mocks.

ES6 classes are just syntactic sugar on top of constructor functions, so we can use the same methods for mock functions to mock classes.

Making Something to Test

First, we make something to test by creating videoPlayer.js and add:

class VideoPlayer {
    constructor() {

    }

    playVideoFile(fileName) {
        console.log(`Playing ${fileName}`);
    }
}

module.exports = VideoPlayer;

Then we create app.js as follows:

const VideoPlayer = require('./videoPlayer');

class VideoPlayerApp {
    constructor() {
        this.videoPlayer = new VideoPlayer();
    }

    play() {
        const videoFileName = 'video.mp4';
        this.videoPlayer.playVideoFile(videoFileName);
    }
}

module.exports = VideoPlayerApp;

Creating Automatic Mocks

In this example, we’ll mock the VideoPlayer class.

To do this, we can call jest.mock(‘./videoPlayer’); since we export the VideoPlayer class with module.exports .

Jest is smart enough to create its own mock class in place of the actual VideoPlayer class in tests.

It’ll replace the class with a mock constructor function, and replaces all its methods with mock functions that always return undefined .

Method calls are saved in theAutomaticMock.mock.instances[index].methodName.mock.calls, where theAutomaticMock is replaced with the same name as the class we originally mocked.

In our example, we aren’t going to create our own implementation of the methods, so once we called jest.mock(‘./videoPlayer’); , we’re done with mocking the VideoPlayer class.

Then we can create app.test.js to put our tests. We add the following:

const VideoPlayer = require('./videoPlayer');
const VideoPlayerApp = require('./app');
jest.mock('./videoPlayer');

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

test('VideoPlayer is called once', () => {
    const videoPlayerApp = new VideoPlayerApp();
    expect(VideoPlayer).toHaveBeenCalledTimes(1);
});

test('VideoPlayer is called with video.mp4', () => {
    expect(VideoPlayer).not.toHaveBeenCalled();
    const videoPlayerApp = new VideoPlayerApp();
    videoPlayerApp.play();
    const videoFileName = 'video.mp4';
    const mockVideoPlayer = VideoPlayer.mock.instances[0];
    const mockPlayVideoFile = mockVideoPlayer.playVideoFile;
    expect(mockPlayVideoFile.mock.calls[0][0]).toEqual(videoFileName);
    expect(mockPlayVideoFile).toHaveBeenCalledWith(videoFileName);
    expect(mockPlayVideoFile).toHaveBeenCalledTimes(1);
});

We important the modules for containing each class. The VideoPlayer class will be replaced with our mock, but we still have to import it.

Then we have our beforeEach hook which runs:

VideoPlayer.mockClear();

to clear the mocks of the VideoPlayer class.

Next, we write our first test, which is:

test('VideoPlayer is called once', () => {
    const videoPlayerApp = new VideoPlayerApp();
    expect(VideoPlayer).toHaveBeenCalledTimes(1);
}););

We create a new VideoPlayerApp instance. The constructor of the VideoPlayerApp calls runs new VideoPlayer(); , which means the VideoPlayer class is called.

Then we check that:

expect(VideoPlayer).toHaveBeenCalledTimes(1);

which should be true since the VideoPlayer constructor function, aka the class, is called once.

Next, we look at our second test, which is more complex:

test('VideoPlayer is called with video.mp4', () => {
    expect(VideoPlayer).not.toHaveBeenCalled();
    const videoPlayerApp = new VideoPlayerApp();
    videoPlayerApp.play();
    const videoFileName = 'video.mp4';
    const mockVideoPlayer = VideoPlayer.mock.instances[0];
    const mockPlayVideoFile = mockVideoPlayer.playVideoFile;
    expect(mockPlayVideoFile.mock.calls[0][0]).toEqual(videoFileName);
    expect(mockPlayVideoFile).toHaveBeenCalledWith(videoFileName);
    expect(mockPlayVideoFile).toHaveBeenCalledTimes(1);
});

First, we check that VideoPlayer hasn’t been called to make sure that VideoPlayer.mockClear() is working.

Then we create a new instance of VideoPlayerApp and call play on it as follows:

const videoPlayerApp = new VideoPlayerApp();
videoPlayerApp.play();

After that, we get our mock VideoPlayer instance by running:

const mockVideoPlayer = VideoPlayer.mock.instances[0];

Then we call the playVideoFile method of the VideoPlayer class that was mocked as follows:

const mockPlayVideoFile = mockVideoPlayer.playVideoFile;

Then we can get the first argument that’s passed into the mock playVideoFile call by using:

mockPlayVideoFile.mock.calls[0][0]

We can then check that the call was done with 'video.mp4' passed by writing:

expect(mockPlayVideoFile.mock.calls[0][0]).toEqual(videoFileName);

Equivalently, we can write:

expect(mockPlayVideoFile).toHaveBeenCalledWith(videoFileName);

Finally, we check that the playVideoFile method was called only once by running:

expect(mockPlayVideoFile).toHaveBeenCalledTimes(1);

This makes sense since playVideoFile is only called once in our test.

Photo by Andrew Tanglao on Unsplash

Running the Tests

Be sure to install Jest by running:

npm i jest

and put:

"test": "jest"

in the scripts section so we can run the tests.

In the end, we should get:

PASS  ./app.test.js
  √ VideoPlayer is called once (3ms)
  √ VideoPlayer is called with video.mp4 (2ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.067s
Ran all test suites.

Conclusion

With Jest’s automatic mocks, we can mock classes or constructor functions easily.

All methods are mocked with functions that return undefined.

Then we can retrieve the mock by using mockedObject.mock.instances, which is an array.

Then we can run checks as usual. with toEqual, toHaveBeenCalledWith, and other matcher methods.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

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