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 usedescribe 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 usingit
ortest
.
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 usevi.fn()
. It creates a mock functions and returnsundefined
.
// 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 usespy
.
// 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 vitest-mock-extended
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
Top comments (2)
Let me know, If I miss something π
could you share an example, where an api consumes secret manager data, as well as the secret manager data contains db credentials and then query some data from a table?