Mastering Unit Testing: A Comprehensive Guide
Tags: testing, vitest, cicd, typescript
Published: 2024-07-05 15:27:33

## 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.

```ts
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`.

```ts
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`.

```ts
// 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`.

```ts
// 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`.

```ts
//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

```ts
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

```ts
"rootDir": "./src",
"outDir": "./dist",
```

Add a script to test in package.json

```ts
"test": "vitest"
```

Adding an DB

```ts
npm i prisma
npx prisma init
```

Add this basic schema in `schema.prisma`

```ts
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)

```ts
npx prisma generate
```

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

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

src/Index.ts

```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.

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

export const prismaClient = mockDeep<PrismaClient>();
```

**`index.test.ts`**

```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.

```yml
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
```