Unit testing a NestJS applications with Jest (Phần 1)
Dạo gần đây phải code NestJS khá nhiều nên cũng lang thang đi tìm hiểu unit tests cho NestJS application. Sau một hồi tha hương cầu thực trên mạng thì cũng thấy một vài bài khá hay nên mạnh dạn tổng hợp mang về đây viết. Vì bài hơi dài nên mình sẽ chia ra làm 2 phần để chi tiết hơn từng phần. Let 's go!
Có 5 bước chủ yếu ta cần phải làm để có đạt được như cái tiêu đề blog:
1. Tạo một ứng dụng NestJS application đơn giản.
2. Add unit test cases.
3. Run unit test cases.
4. Add end-to-end test cases.
5. Run all tests.
Vào từng bước nào:
1. Tạo một ứng dụng NestJS đơn giản
- Để khởi tạo một ứng dụng NestJS, run
nest new name_project
trong VSCode terminal (trong đóname_project
là tên ứng dụng muốn đặt nhé). Ví dụ mình sẽ đặt là StudentGPA:
nest new StudentGPA
- Tiếp theo tạo 2 services bằng cách thực hiện lệnh:
nest g service student
nest g service api
-
Chạy ứng dụng và sẽ được kết quả như này:
-
Cấu trúc thư mục sẽ có dạng:
-
Giải thích qua về ứng dụng: Với đầu vào là
firstName
vàsecondName
của sinh viên, kết quả trả về GPA của sinh viên đó.StudentService
sẽ sử dụngApiService
để lấy thông tin sinh viên đó. -
Unit testing sẽ được thực hiện trong NestJS bởi framework JestJS.
-
Bắt tay vào code nhé! Đầu tiên mình sẽ thêm hàm
getGPA
trongAppController
.
import { Controller, Get, HttpException, Query } from '@nestjs/common';
import { AppService } from './app.service';
import { StudentService } from './student/student.service';
@Controller('student')
export class AppController {
constructor(
private readonly appService: AppService,
private readonly studentService: StudentService,
) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('/gpa')
async getStudentGpa(
@Query('firstName') firstName: string,
@Query('lastName') lastName: string,
): Promise<number> {
if (!firstName || !lastName) {
throw new HttpException('Incomplete student information', 400);
}
return await this.studentService.getGpa(firstName, lastName);
}
}
- Và giờ mình cần tạo hàm
getGPA
trongstudentService
:
import { HttpException, Injectable } from '@nestjs/common';
import { ApiService } from '../api/api.service';
export interface Student {
name: string;
grades: number[];
}
@Injectable()
export class StudentService {
constructor(private apiService: ApiService) {}
public async getGpa(firstName: string, lastName: string): Promise<number> {
const student: Student = await this.apiService.getStudent(
firstName,
lastName,
);
if (!student || !student.grades) {
throw new HttpException('Cannot find student or student grades', 404);
}
let gpa = 0;
for (const grade of student.grades) {
gpa += grade / student.grades.length;
}
return gpa;
}
}
- Tiếp theo là tạo hàm
getStudent
trongapiService
:
import { HttpService, Injectable } from '@nestjs/common';
import { Student } from '../student/student.service';
@Injectable()
export class ApiService {
constructor(private http: HttpService) {}
async getStudent(firstName: string, lastName: string): Promise<Student> {
const url = `../get-student?firstName=${firstName}&lastName=${lastName}`;
const response = await this.http.get(url).toPromise();
return response.data;
}
getHello(): string {
return 'Hello World!';
}
}
- Chuẩn bị xong, run
nest start
nào.
2. Add unit test cases
- Tìm hiểu lại một số kiến thức về unit testing trước khi bắt đầu nhé. Unit testing tập trung vào viết tests cho các phần nhỏ nhất (phần lớn là các functions được định nghĩa trong classes). VD MethodA ở classA có thể gọi được MethodB ở classB. Tuy nhiên, unit test của MethodA chỉ tập trung vào logic của MethodA, không phải MethodB.
- Unit tests không nên phụ thuộc vào môi trường mà chúng chạy. Để viết những unit tests biệt lập, ta sẽ mock all những dependencies của method/service. Và trong unit test
StudentService
, ta sẽ mockAppService
bằng cách tạo classApiServiceMock
.- Test Doubles: Fakes, stubs, và mock all là thuộc tính của test doubles. Một test double là một object hay system mà bạn dùng trong test để thay thế cho một cái gì đó.
- Fakes: Một object với khả năng giới hạn(cho mục đích testing), VD: fake web service. Fake có hoạt động kinh doanh. Bạn có thể khiến Fake hoạt động theo những cách khác nhau bằng cách cung cấp cho nó những data khác nhau. Fakes có thể được sử dụng khi bạn không thể dùng triển khai thực tế trong test của mình.
- Stub: một object cung cấp các câu trả lời được xác định để method gọi. Stub không có logic và chỉ trả về những gì bạn yêu cầu trả về.
Nếu chưa hiểu lắm về fake/mock/stub, bạn có thể tham khảo thêm tại here . - Spy: Thường được sử dụng để đảm bảo một phương thức cụ thể đã được gọi.
- Đối với unit tests, ta sẽ tập chung vào 3 classes:
- app.controller.spect.ts (Test class của AppController)
- student.service.spec.ts (Test class của StudentService)
- api.service.spec.ts (Test class của ApiService)
- Có nhiều cách tiếp cận mà chúng ta có thể sử dụng ở đây. Một trong số đó là sử dụng Mock functions được cung cấp bởi Jest framework. Mock functions cho phép ta test links giữa code bằng cách xóa việc triển khai thực tế của một function. Một số khác thì sử dụng mock classes.
- Hãy cùng khám phá cách tiếp cận mock funtion trong app.controller.spec.ts:
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { StudentService } from './student/student.service';
describe('AppController', () => {
let appController: AppController;
let spyService: StudentService;
beforeEach(async () => {
const ApiServiceProvider = {
provide: StudentService,
useFactory: () => ({
getGpa: jest.fn(() => 4.5),
}),
};
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService, ApiServiceProvider],
}).compile();
appController = app.get<AppController>(AppController);
spyService = app.get<StudentService>(StudentService);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
describe('getGPA', () => {
it('should call getGPA for a student', async () => {
const firstName = 'Joe';
const secondName = 'Foo';
appController.getStudentGpa(firstName, secondName);
expect(spyService.getGpa).toHaveBeenCalled();
});
});
describe('getGPA', () => {
it('should retrieve getGPA for a student', async () => {
const firstName = 'Joe';
const secondName = 'Foo';
expect(spyService.getGpa(firstName, secondName)).toBe(4.5);
});
});
});
- Line #1:
Test
class từ @nestjs/testing cung cấp ngữ cảnh thực thi test bằng cách mock toàn bộ thời gian chạy Nest. Nó cũng cung cấp các hook để mock và ghi đè. - Line #8: Biến
spyService
khai báo để theo dõi lệnh gọi củaStudentService
. - Line #17-20: Phương thức biên dịch của đối tượng này khởi động một module với các dependencies của nó và trả về một module chuẩn bị cho testing.
- Line #23:
spyService
được định nghĩa. - Line #13-14: mock function cho
getGPA
được định nghĩa. - Line #28: Xác minh
getHello()
method củaAppController
. - Line #35: Xác minh
getGPA
củaStudentService
đã được gọi. - Line #44: Xác minh giá trị trả về của
getGPA
đúng như giá trị mong đợi. - Tiếp theo, hãy cùng khám phá cách tiếp cận mock funtion trong student.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { StudentService } from './student.service';
import { ApiService } from '../api/api.service';
class ApiServiceMock {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getStudent(_firstName: string, _lastName: string) {
return {
name: 'Jane Doe',
grades: [3.7, 3.8, 3.9, 4.0, 3.6],
};
}
}
describe('StudentService', () => {
let studentService: StudentService;
beforeEach(async () => {
const ApiServiceProvider = {
provide: ApiService,
useClass: ApiServiceMock,
};
const module: TestingModule = await Test.createTestingModule({
providers: [StudentService, ApiServiceProvider],
}).compile();
studentService = module.get<StudentService>(StudentService);
});
it('StudentService - should be defined', () => {
expect(studentService).toBeDefined();
});
describe('getGpa', () => {
it('should get student GPA', async () => {
const expectedGpa = 3.8;
const gpa = await studentService.getGpa('Jane', 'Doe');
expect(gpa).toEqual(expectedGpa);
});
});
});
- Line #5-13: Mock class.
- Line #23: Phương thức
createTestingModule
củaTest
class lấy một đối tượng module metadata và trả về một phiên bản(instance)TestingModule
. - Line #24: Việc triển khai mock class của
ApiService
được cung cấp để sử dụng thay vì triển khai thực tế. - Line #31: Xác minh rằng đối tượng
studentService
được định nghĩa. - Line #38: Xác minh rằng GPA nhận được giống với GPA mong chờ.
- Cuối cùng là api.serivce.spec.ts, test class của
ApiService
.
import { Test, TestingModule } from '@nestjs/testing';
import { ApiService } from './api.service';
import { HttpModule } from '@nestjs/common';
describe('ApiService', () => {
let service: ApiService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ApiService],
imports: [HttpModule],
}).compile();
service = module.get<ApiService>(ApiService);
});
it('ApiService - should be defined', () => {
expect(service).toBeDefined();
});
});
Phần 1 này mình chỉ làm đến đây. Sẽ ra phần 2 là 3 mục còn lại sớm nhất có thể. Mọi người đọc và cho mình xin góp ý. Tài liệu tham khảo có thể xem ở đây:
https://nishabe.medium.com/unit-testing-a-nestjs-app-in-shortest-steps-bbe83da6408
https://blog.theodo.com/2019/06/test-nestjs-with-jest-typescript/