Mocking with Jest
Background
Let's say we have a events service. This service also interacts with a Cloudinary service for event image upload and Open AI for generating embeddings for representing the event.
Now say we would like to test this events service. It would certainly be ideal if we can isolate the events service so that we can focus on testing the main parts of the events service. How can we effectively do that?
Introducing, Mocking!
Mocking is a way to replicate an external dependency with another representative object to simulate the real dependency's functionality, as this source suggests. In this way, we can focus on testing the main parts of the main service.
In the Javascript world, we can mock dependencies with Jest. This means that we can use Jest not only for basic unit testing, but also for mocking.
Let's implement it!
Before that, we need to set these testing parts:
// we're mocking the real Xendit functionality
// with this object, basically createInvoice is an empty function
const mockXendit = {
Invoice: {
createInvoice: jest.fn(),
},
};
// we also do this for db
const mockDb = {
query: {
userCartItems: {
findMany: jest.fn(),
},
eventTickets: {
findFirst: jest.fn(),
},
},
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
transaction: jest.fn().mockImplementation((callback) => callback(mockTx)),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PaymentService,
// we're replacing the real db implementation with the mocked version
{ provide: PG_CONNECTION, useValue: mockDb },
// we're replacing the real Xendit implementation with the mocked version
{
provide: Xendit,
useValue: mockXendit,
},
],
}).compile();
service = module.get<PaymentService>(PaymentService);
xendit = module.get<Xendit>(Xendit);
});
Let's say we have this method:
private async createInvoiceUrl({ user, items }: CreateInvoiceUrlInterface) {
const { Invoice: invoiceClient } = this.xendit;
const invoiceId = `EVT-${this.generateNanoId()}`;
const totalPrice = items.reduce((acc, prev) => {
const ticketPrice = prev.price * prev.quantity;
return acc + ticketPrice;
}, 0);
const invoiceData = this.createInvoiceData({
items,
user,
invoiceId,
totalPrice,
});
try {
const { invoiceUrl, id, externalId, status } =
await invoiceClient.createInvoice({
data: invoiceData,
});
return { invoiceUrl, id, externalId, status, totalPrice };
} catch (err) {
throw new InternalServerErrorException(err);
}
}
We can make this unit test:
describe('createInvoiceUrl', () => {
const dummyInvoiceData = {} as CreateInvoiceRequest;
const dummyCreateInvoiceUrlResponse = {
invoiceUrl: dummyInvoiceUrl,
id: 'id',
externalId: 'externalId',
status: 'status',
totalPrice: 10,
};
it('should create the invoice url', async () => {
service['xendit'] = xendit;
// mocking internal method
jest
.spyOn(service as any, 'createInvoiceData')
.mockReturnValueOnce(dummyInvoiceData);
// mocking Xendit
mockXendit.Invoice.createInvoice.mockReturnValue(
dummyCreateInvoiceUrlResponse,
);
const createInvoiceUrlResponse = await service['createInvoiceUrl']({
user: dummyUser as User,
items: dummyTicketItems,
});
expect(createInvoiceUrlResponse).toStrictEqual(
dummyCreateInvoiceUrlResponse,
);
});
});
In this unit test, we have mocked the Xendit implementation, which is an external dependency. In this way, we can focus on the main objective of the function, which is to return the invoice URL, along with some other important data.
Let's try to mock another external dependency.
We have an events service that has these dependencies:
@Injectable()
export class EventsService {
constructor(
...
private openAIUtil: OpenAIUtil,
) {}
Next, let's say we have a method in our events service:
private async createEmbedding(input: string) {
const data = await this.openAIUtil.generateEmbeddings(input);
return data[0].embedding;
}
We can setup our unit testing environment:
const mockOpenAIUtil = { generateEmbeddings: jest.fn() };
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
EventsService,
...
{ provide: OpenAIUtil, useValue: mockOpenAIUtil },
],
}).compile();
service = module.get<EventsService>(EventsService);
...
openAIUtil = module.get<OpenAIUtil>(OpenAIUtil);
});
We can create this unit test:
describe('create embedding', () => {
it('should call the OpenAI util to create the embedding', async () => {
const dummyInput = 'input';
const dummyEmbedding = [0.01, 0.02, 0.03];
const mockGenerateEmbeddingsResponse = [{ embedding: dummyEmbedding }];
// mocking OpenAI util
mockOpenAIUtil.generateEmbeddings.mockResolvedValueOnce(
mockGenerateEmbeddingsResponse,
);
const embedding = await service['createEmbedding'](dummyInput);
expect(openAIUtil.generateEmbeddings).toHaveBeenCalledWith(dummyInput);
expect(embedding).toBe(dummyEmbedding);
});
});
In this way, we're focusing on the main functionality of the createEmbedding
method, which is to return the generated embedding from the OpenAI.
Conclusion
Jest is really flexible for mocking external dependencies in our classes. In this way, our testing is more isolated and focused on the main method being tested.