How to mock TypeScript interfaces with Jest

How to mock TypeScript interfaces with Jest
Listen to this article

Last week I was creating a NodeJS + ExpressJS app in TypeScript and I was wondering how to apply the Onion Architecture successfully. In practice that was not a problem (I will write an article about it soon) until the moment of testing.

There is little to no documentation about how to mock TypeScript interfaces in Jest and what I found was most of the time misleading or not what I was looking for.

First I used jest-mock-extended but I was not very convinced and I ended up playing around with jest until I came up with a working solution.

I think that this could be applied to both NodeJS and browser JS apps. First, you obviously need jest and ts-jest as devDependencies.

Now let's say I have this code under src/DomainModel/Reply and I want to test a class called ReplyService, mocking its dependencies.

src/DomainModel/Reply/ReplyInterface.js

export interface ReplyInterface {
    text: string;
}

src/DomainModel/Reply/ReplyRepositoryInterface.js

import {ReplyInterface} from "./ReplyInterface";

export interface ReplyRepositoryInterface {
    findOneByIntent: (intentName: string) => Promise<ReplyInterface>
}

src/DomainModel/Reply/ReplyService.js

import {ReplyRepositoryInterface} from "./ReplyRepositoryInterface";
import {ReplyInterface} from "./ReplyInterface";
import {ReplyNotFoundError} from "./ReplyNotFoundError";

export class ReplyService {
    constructor(
        private replyRepository: ReplyRepositoryInterface
    ) {
    }

    public async getReply(intent: string): Promise<string> {
        let reply: ReplyInterface

        try {
            reply = await this.replyRepository.findOneByIntent(intent);
        } catch (e) {
            if (!(e instanceof ReplyNotFoundError)) {
                throw e
            }
            reply = {text: `Sorry, I cannot help you with '${intent}' in this moment...`};
        }

        return reply.text;
    }
}

Here you can see that ReplyService has a dependency on ReplyRepositoryInterface but, how can we mock this interface to test our service in isolation as a real unit test?

To mock a TypeScript interface in jest, you only need an object that has the same functions as the interface. In our case, we need to mock a function that returns a promise. We can do that with jest.fn():

const replyRepositoryMock = {
  findOneByIntent: jest.fn().mockReturnValue(Promise.resolve({text: replyText}))
};

And this is how one of the tests would look like:

src/DomainModel/Reply/ReplyService.test.js

import {ReplyService} from "./ReplyService";

test('getReply returns the expected reply text', () => {
    const intent = 'baz'
    const replyText = 'ok'

    const replyRepositoryMock = {
        findOneByIntent: jest.fn().mockReturnValue(Promise.resolve({text: replyText}))
    };
    const replyService = new ReplyService(replyRepositoryMock);

    return replyService
        .getReply(intent)
        .then(reply => {
            expect(replyRepositoryMock.findOneByIntent).toBeCalledWith(intent);
            expect(reply).toBe(replyText)
        })
});

Jest is very flexible and it also allows you to mock entire packages, like axios:

src/Infrastructure/UltimateAi/IntentSearchService.test.js

import {IntentSearchService} from "./IntentSearchService";
import axios from "axios";
jest.mock('axios');

test('example axios mock', () => {
    const resp = {
        data: {
            intents: [
                {name: "Foo", ratio: 0.7},
                {name: "Bar", ratio: 0.2},
                {name: "Baz", ratio: 0.5},
                {name: "Foo2", ratio: 0.71},
                {name: "Bar2", ratio: 0.2},
            ]
        }
    };

    const axiosMock = axios as jest.Mocked<typeof axios>
    axiosMock.create.mockReturnValue(axiosMock)
    axiosMock.post.mockResolvedValue(resp)

    const client = new IntentSearchService(axiosMock)
});

As you can see you can mock pretty much anything with Jest, it's pretty simple and you don't need any other libraries to accomplish the same.

 
Share this