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.