Mastering Unit Testing: A Comprehensive Guide

RMAG news

What

In Unit test we test the functions, endpoints, components individually. In this we just test the functionality of the code that we have written and mock out all the external services [ DB Call, Redis] etc.

How

We are going to use [Vitest](https://vitest.dev/) for doing the testing as it has more smooth Typescript support rather than Jest. You can use any of them the code will look the same for both.

Some Testing Jargons

Test suite – It is a collection of the test cases of a particular module. we use describe function, it helps to orgainse our test cases into groups.

describe(Testing Sum Module, () => {
// Multiple test cases for testing sum module
});

describe(Testing Multiplication Module, () => {
// Multiple test cases for testing Multiplication module
});

Test case – It is a individual unit of testing, defined using it or test.

describe(Testing Sum Module, () => {
// Multiple test cases for testing sum module
it(should give 1,2 sum to be 3, () => {
// It checks that result matches or not
expect(add(1, 2))toBe(3);
});
});

Mocking – It is used to mock out various external calls like DB calls. To do this we use vi.fn(). It creates a mock functions and returns undefined.

// db contains the code that we have to mock.
// it is good practice to keep the content that we have to mock in a seprate file
// vi.mock() is hoisted on the top of the test file.
vi.mock(../db, () => {
return {
// prismaClient is imported from db file
// we want to mock the prismaClient.sum.create() function
prismaClient:{
sum:{
create:vi.fn();
}
}
};
});

Spy – It is basically used to spy on a function call, means as we are mocking the db call but we don’t know that right arguments are passed to that db call or not, so to check that we use spy.

// create is the method
// prismaClient.sum is the object
vi.spyOn(prismaClient.sum, create);
// Now to check write arguments are passed on to the create method or not we can do this
expect(prismaClient.sum.create).toHaveBeenCalledWith({
data: {
a: 1,
b: 2,
result: 3,
},
});

Mocking Return Values – Sometimes you want to use values returned by an async operation/external calls. Right now we can’t use any of the values as we are mocking that call. To do so we have to use mockResolvedValue.

//this is not the actual prismaClient object. This is the Mocked version of prismaClient that we are using that’s why we are able to use mockResolvedValue.
prismaClient.sum.create.mockResolvedValue({
id: 1,
a: 1,
b: 1,
result: 3,
});

Examples

Unit test of an Express APP
In an express we write the app.listen in seprate file, cuz when we try to run the test everytime it will start a server & we can’t hard code any PORT [ what if PORT is in use ]. So we use superset which automatically creates an use and throw server.
we create an seperate bin.ts or main.ts file which will do the app.listen.

Run these Commands

npm init y
npx tsc init
npm install express @types/express zod
npm i D vitest
// supertest allow us to make server
npm i supertest @types/supertest
// used for deep-mocking, by using this we don’t have to tell which method to mock, we can mock whole prisma client
npm i D vitestmockextended

Change rootDir and srcDir

rootDir: ./src,
outDir: ./dist,

Add a script to test in package.json

test: vitest

Adding an DB

npm i prisma
npx prisma init

Add this basic schema in schema.prisma

model Sum {
id Int @id @default(autoincrement())
a Int
b Int
result Int
}

Generate the client (notice we don’t need to migrate since we wont actually need a DB)

npx prisma generate

Create src/db.ts which exports the prisma client. This is needed because we will be mocking this file out eventually

import { PrismaClient } from @prisma/client;
export const prismaClient = new PrismaClient();

src/Index.ts

import express from express;
import { z } from zod;
import { prismaClient } from ./db;

export const app = express();

app.use(express.json());

const sumInput = z.object({
a: z.number(),
b: z.number(),
});

app.post(/sum, async (req, res) => {
const parsedResponse = sumInput.safeParse(req.body);
if (!parsedResponse.success) {
return res.status(411).json({ message: Invalid Input });
}
// const a = req.body.a;
// const b = req.body.b;
const answer = parsedResponse.data.a + parsedResponse.data.b;
// we want to mock this as empty function
const response = await prismaClient.sum.create({
// kya gurantee hai ki yeh data aise hi hoga , agar koi contributer isme change kar de to
// to solve this issue we have spy that
// abhi agar hum isme wrong input bhi pass karege tab bhi ye koi error nhi dega
// so we have to use spies
data: {
a: parsedResponse.data.a,
b: parsedResponse.data.b,
result: answer,
},
});
console.log(response.Id);
// agar user try karega to return something else it will give them a error.
// res.json({ answer, id: response.b });
res.json({ answer, id: response.Id });
});

// isme sab kuch headers mai pass hoga
app.get(/sum, (req, res) => {
const parsedResponse = sumInput.safeParse({
a: Number(req.headers[a]),
b: Number(req.headers[b]),
});
if (!parsedResponse.success) {
return res.status(411).json({ message: Invalid Input });
}
const answer = parsedResponse.data.a + parsedResponse.data.b;

res.json({ answer });
});

Create __mocks__/db.ts in the src folder, same folder in which db.ts resides. Its a type of convention, vitest looks for any __mocks__ file to know what to mock.

import { PrismaClient } from @prisma/client;
import { mockDeep } from vitest-mock-extended;

export const prismaClient = mockDeep<PrismaClient>();

index.test.ts

import { describe, it, expect, vi } from vitest;
import request from supertest;
import { app } from ../index;
import { prismaClient } from ../__mocks__/db;

// vi.mock(“../db”, () => ({
// prismaClient: { sum: { create: vi.fn() } },
// }));

vi.mock(../db);

// // Mocking the return value using mockResolvedValue
// prismaClient.sum.create.mockResolvedValue({
// Id: 1,
// a: 1,
// b: 2,
// result: 3,
// });
describe(POST /sum, () => {
it(Should return the sum of 2,3 to be 6, async () => {
// Mocking the return value using mockResolvedValue
prismaClient.sum.create.mockResolvedValue({
Id: 1,
a: 1,
b: 2,
result: 3,
});

vi.spyOn(prismaClient.sum, create);
const res = await request(app).post(/sum).send({
a: 1,
b: 2,
});

expect(prismaClient.sum.create).toBeCalledWith({
data: {
a: 1,
b: 2,
result: 3,
},
});
expect(prismaClient.sum.create).toBeCalledTimes(1);
expect(res.status).toBe(200);
expect(res.body.answer).toBe(3);
expect(res.body.id).toBe(1);
});

it(Should return sum of 2 negative numbers, async () => {
// Mocking the return value using mockResolvedValue
prismaClient.sum.create.mockResolvedValue({
Id: 1,
a: 1,
b: 2,
result: 3,
});

vi.spyOn(prismaClient.sum, create);
const res = await request(app).post(/sum).send({
a: 10,
b: 20,
});

expect(prismaClient.sum.create).toBeCalledWith({
data: {
a: 10,
b: 20,
result: 30,
},
});
expect(prismaClient.sum.create).toBeCalledTimes(1);

expect(res.status).toBe(200);
expect(res.body.answer).toBe(30);
expect(res.body.id).toBe(1);
});

it(If wrong input is provided, it should return 411 with a msg, async () => {
const res = await request(app).post(/sum).send({
a: abcd,
b: 2,
});

expect(res.status).toBe(411);
expect(res.body.message).toBe(Invalid Input);
});
});

describe(GET /sum, () => {
it(should return the sum of 2,3 to be 5, async () => {
const res = await request(app)
.get(/sum)
.set({
a: 2,
b: 3,
})
.send();

expect(res.status).toBe(200);
expect(res.body.answer).toBe(5);
});
});

Now Run npm run test to test your code.

Implementing CI Pipeline

Create an .github/workflows/test.yml file

This below code will automatically tests the if any code is pushed on main branch or for any pull request.

name: Testing on CI
on:
pull_request:
branches:
main
push:
branches:
main

jobs:
test:
runs-on: ubuntu-latest
steps:
name: Checkout code
uses: actions/checkout@v2

name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: 20

name: Install dependencies
run: npm install && npx prisma generate

name: Run tests
run: npm test