Giới thiệu

“Automation test” là một phần thiết yếu của bất cứ dự án phát triển phần mềm nào. Tự động hoá giúp thực hiện lặp đi lặp lại các bài kiểm tra riêng lẻ hay một bộ các bài kiểm tra một cách nhanh chóng và dễ dàng trong quá trình phát triển. Điều này giúp đảm bảo rằng các phiên bản của phần mềm sẽ đáp ứng các mục tiêu về chất lượng và hiệu suất. Tự động hoá giúp gia tăng sự bao phủ và cung cấp cho các nhà phát triển vòng lặp phản hồi nhanh hơn. Tự động hoá vừa làm tăng năng suất của các nhà phát triển vừa đảm bảo rằng các thử nghiệm sẽ được chạy ở các thời điểm quan trọng của vòng đời sản phẩm, như lúc kiểm tra mã nguồn, tích hợp tính năng mới, và phát hành phiên bản mới.

Các bài test như vậy thường phải trải qua nhiều loại khác nhau vd như: unit test, end-to-end (e2e), integration test … Mặc dù việc làm này mang lại lợi ích lớn, nhưng việc thiết lập thì rất mất thời gian. Nest luôn cố gắng tạo ra các phương pháp phát triển tốt nhất, bao gồm cả việc test hiệu quả, vì vậy, Nest đã xây dựng một tính năng để giúp các nhà phát triển xây dựng “automation test”:

  • Xây dựng tự động các unit test cho các thành phần, cũng như e2e tests cho cả ứng dụng
  • Cung cấp các công cụ mặc định (VD: trình chạy thử nghiệm / trình tải ứng dụng)
  • Tích hợp với JestSupertest, trong khi điều này đang bất khả thi với các công cụ khác.
  • Tạo ra hệ thống “Nest dependency injection” trong môi trường kiểm thử để dễ dạng tích hợp các thành phần “mockup”.

Như đã đề cập, ứng dụng có thể dùng bất cứ “testing framework” nào, Nest không bắt buộc sử dụng bất cứ công cụ. Đơn giản thay thế các yếu tố (vd như trình chạy kiểm thử), mà ứng dụng vẫn được sử dụng các tiện ích khác của môi trường kiểm thử của Nest.

Cài đặt

Để bắt đầu, đầu tiên bắt buộc phải cài đặt gói:

$ npm i --save-dev @nestjs/testing

Unit testing

Trong ví dụ sau đây, chúng ta sẽ kiểm thử 2 lớp: CatsControllerCatsService. Như đã đề cập, Jest sẽ là framework mặc định. Nó được sử dụng như là một test-runner và cung cấp các assert functions và các tính năng  test-double để giúp cho việc mocking, spying, …. Trong bài test đơn giản sau đây, chúng ta sẽ khởi tạo thủ công các lớp này, và chắc chắn rằng controller và service sẽ thực hiện đúng các chức năng của nó.

import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

describe('CatsController', () => {
  let catsController: CatsController;
  let catsService: CatsService;

  beforeEach(() => {
    catsService = new CatsService();
    catsController = new CatsController(catsService);
  });

  describe('findAll', () => {
    it('should return an array of cats', async () => {
      const result = ['test'];
      jest.spyOn(catsService, 'findAll').mockImplementation(() => result);

      expect(await catsController.findAll()).toBe(result);
    });
  });
});

Gợi ý

Để file test gần với file chính. File test nên có đuôi .spec hoặc .test

Vì ví dụ ở trên đơn giản, nên ta không thật sự test hết các đặc trưng của Nest. Thực vậy, ta thậm chí còn chưa sử dụng dependency injection ( lưu ý rằng ta đưa một instance của CatsService cho CatsController). Dạng test này ( khởi tạo thủ công các class đang test) thường được gọi là “isolated test” (kiểm thử độc lập) vì nó độc lập với framework đang sử dụng. Vì vậy sau đây chúng ta sẽ thực hiện một số tính năng nâng cao giúp test các ứng dụng mà trong đó sử dụng các tính năng của Nest.

Tiện ích test của nest

Gói @nestjs/testing cung cấp các tính năng mà giúp cho quá trình test tốt hơn. Ta sẽ làm lại ví dụ trên mà sử dụng Test class của Nest:

import { Test } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

describe('CatsController', () => {
  let catsController: CatsController;
  let catsService: CatsService;

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
        controllers: [CatsController],
        providers: [CatsService],
      }).compile();

    catsService = moduleRef.get<CatsService>(CatsService);
    catsController = moduleRef.get<CatsController>(CatsController);
  });

  describe('findAll', () => {
    it('should return an array of cats', async () => {
      const result = ['test'];
      jest.spyOn(catsService, 'findAll').mockImplementation(() => result);

      expect(await catsController.findAll()).toBe(result);
    });
  });
});

Test class rất hữu dụng trong việc cung cấp cho ứng dụng một ngữ cảnh thực thi mà giả lập toàn bộ Nest runtime, nhưng cung cấp cho bạn các hooks làm cho việc quản lý các instance của class một cách dễ dàng, bao gồm cả mocking và overriding. Test class có một phương thức createTestingModule() mà nó lấy 1 đối tượng “Module” làm đối số ( giống với đối tượng mà ta cho vào @Module() decorator ). Phương thức này sẽ trả về một instance của TestingModule. Đối với unit tests, quan trọng nhất là phương thức compile(). Phương thức này sẽ khởi tạo một module với các dependencies của nó ( giống như cách ứng dụng được khởi tạo trong file main.ts sử dụng NestFactory.create() ) và trả về một module sẵn sàng cho việc test

Gợi ý

Phương thức compile() bất đồng bộ và vì vậy nên có “await”. Khi module được biên dịch xong, ta có thể lấy bất cứ một static instance nào mà nó định nghĩa ( controllers và providers ) bằng cách sử dụng phương thức get()

TestingModule kế thừa từ module reference class, và do đó nó có khả năng linh động resolve các scoped providers (transient or request-scoped). Thực hiện với phương thức resolve() ( phương thức get() chỉ có thể tạo ra các static instance)

const moduleRef = await Test.createTestingModule({
  controllers: [CatsController],
  providers: [CatsService],
}).compile();

catsService = await moduleRef.resolve(CatsService);

Cảnh báo

Phương thức resolve() sẽ trả về 1 unique instance của provider, từ các DI container sub-tree của riêng nó. Mỗi sub-tree sẽ có một ngữ cảnh định danh duy nhất. Do đó nếu ta gọi phương thức này nhiều lần và so sánh các instance với nhau, ta sẽ thấy nó không giống nhau

Thay vì sử dụng phiên bản production của bất cứ provider nào, ta có thể override nó với một custom provider cho mục đích testing. ví dụ, ta có thể mock một database service thay vì connect trực tiếp đến live database. Ta sẽ đề cập đến override trong chương tiếp theo, nhưng chúng cũng đã sẵn sàng cho unit test