How to mock TypeScript interfaces with Jest

How to mock TypeScript interfaces with Jest

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.

Did you find this article valuable?

Support Javier Aguilar by becoming a sponsor. Any amount is appreciated!