Giới thiệu tổng quan

Trong môi trường phát triển hiện đại, việc đưa test automation vào quy trình là yếu tố quyết định để duy trì chất lượng code, giảm thiểu lỗi runtime và tăng tốc độ triển khai. Đối với dự án Node.js viết bằng TypeScript, Jest kết hợp với ts-jest cung cấp một giải pháp toàn diện, hỗ trợ cả unit test, integration testcode coverage. Bài viết này sẽ đi sâu vào cách thiết lập môi trường, cấu hình, viết test mẫu, và tích hợp CI/CD, đồng thời chia sẻ các best practices mà các Senior Engineer thường áp dụng.

Cài đặt môi trường và cấu hình ban đầu

Bước 1: Khởi tạo dự án TypeScript

Giả sử bạn đã có một dự án Node.js với TypeScript. Nếu chưa, hãy tạo mới:

npm init -y
npm install --save-dev typescript @types/node
npx tsc --init

Trong file tsconfig.json, bật các tùy chọn sau để hỗ trợ Jest:

{
  "compilerOptions": {
    "target": "ES2019",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "sourceMap": true,
    "outDir": "dist",
    "rootDir": "src"
  }
}

Bước 2: Cài đặt Jest và ts-jest

Thực hiện lệnh sau để cài đặt các package cần thiết:

npm install --save-dev jest ts-jest @types/jest

Sau khi cài đặt, khởi tạo cấu hình Jest:

npx ts-jest config:init

Lệnh trên sẽ tạo file jest.config.js với nội dung cơ bản:

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['**/__tests__/**/*.test.ts'],
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov'],
};

Điều quan trọng là preset: 'ts-jest' giúp Jest hiểu và biên dịch các file .ts trước khi thực thi.

Viết unit test cho các hàm thuần

Ví dụ 1: Hàm cộng đơn giản

File src/math.ts:

export function add(a: number, b: number): number {
  return a + b;
}

File test src/__tests__/math.test.ts:

import { add } from '../math';

describe('add function', () => {
  it('should return sum of two positive numbers', () => {
    expect(add(2, 3)).toBe(5);
  });

  it('should handle negative numbers', () => {
    expect(add(-2, -4)).toBe(-6);
  });
});

Chạy npm test sẽ thấy kết quả:

> jest

 PASS src/__tests__/math.test.ts
  add function
    ✓ should return sum of two positive numbers (5 ms)
    ✓ should handle negative numbers (2 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total

Ví dụ 2: Hàm generic với TypeScript

File src/utils.ts:

export function identity<T>(arg: T): T {
  return arg;
}

File test src/__tests__/utils.test.ts:

import { identity } from '../utils';

describe('identity function', () => {
  it('should return the same string', () => {
    const result = identity('hello');
    expect(result).toBe('hello');
  });

  it('should preserve object reference', () => {
    const obj = { a: 1 };
    const result = identity(obj);
    expect(result).toBe(obj);
  });
});

Những test này chứng minh rằng ts-jest có khả năng hiểu các kiểu generic mà không cần cấu hình thêm.

Mocking và test các module phụ thuộc

Mocking một service HTTP

Giả sử có một service thực hiện gọi API bằng axios:

import axios from 'axios';

export async function fetchUser(id: number) {
  const response = await axios.get(`https://api.example.com/users/${id}`);
  return response.data;
}

Để test mà không thực hiện request thực tế, ta sẽ mock axios:

import axios from 'axios';
import { fetchUser } from '../service';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked;

describe('fetchUser', () => {
  it('should return user data when API succeeds', async () => {
    const mockData = { id: 1, name: 'Alice' };
    mockedAxios.get.mockResolvedValue({ data: mockData });

    const result = await fetchUser(1);
    expect(result).toEqual(mockData);
    expect(mockedAxios.get).toHaveBeenCalledWith('https://api.example.com/users/1');
  });
});

Nhờ jest.mockmockResolvedValue, chúng ta kiểm soát hoàn toàn hành vi của dependency, giảm thời gian chạy test và tránh phụ thuộc vào mạng.

Integration test cho API Node.js

Sử dụng SuperTest với Express

Giả sử có một server Express đơn giản:

import express from 'express';
const app = express();
app.use(express.json());

app.get('/status', (req, res) => {
  res.json({ status: 'ok' });
});

export default app;

File test src/__tests__/api.test.ts:

import request from 'supertest';
import app from '../app';

describe('API integration tests', () => {
  it('GET /status should return ok', async () => {
    const response = await request(app).get('/status');
    expect(response.status).toBe(200);
    expect(response.body).toEqual({ status: 'ok' });
  });
});

Để sử dụng supertest trong môi trường TypeScript, cần cài thêm:

npm install --save-dev supertest @types/supertest

Kết quả test sẽ chạy nhanh, không cần khởi động server thực tế, vì supertest tự tạo một instance của Express trong bộ nhớ.

Đánh giá độ bao phủ (code coverage) và tối ưu hoá

Jest tự động sinh báo cáo coverage khi bật collectCoverage: true. Chạy lệnh:

npm test -- --coverage

Kết quả sẽ xuất ra thư mục coverage với các file lcov.infoindex.html. Mở coverage/index.html trong trình duyệt để xem chi tiết phần trăm bao phủ cho từng file.

Đối với các file có coverage thấp, hãy xem xét:

  • Thêm test cho các nhánh if/else chưa được kiểm tra.
  • Sử dụng jest.each để kiểm tra nhiều giá trị đầu vào.
  • Tránh viết code dead‑code hoặc quá phức tạp mà không có test.

Tích hợp CI/CD với GitHub Actions

Workflow cơ bản

File .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      - name: Install dependencies
        run: npm ci
      - name: Run tests with coverage
        run: npm test -- --coverage
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

Workflow này tự động cài đặt môi trường, chạy test và gửi báo cáo coverage lên Codecov, giúp đội ngũ luôn biết được mức độ an toàn của codebase.

Kết luận

Việc thiết lập Jest + ts-jest cho dự án TypeScript không chỉ mang lại khả năng kiểm thử mạnh mẽ mà còn giúp duy trì tiêu chuẩn code, giảm lỗi runtime và tăng tốc độ phát triển. Các kỹ thuật mocking, integration testingcoverage reporting được trình bày ở trên là những best practices mà các Senior Engineer thường áp dụng trong môi trường production. Khi kết hợp với CI/CD, quy trình kiểm thử trở thành một phần không thể thiếu của DevOps hiện đại. Tham khảo khóa học "Lập trình TypeScript từ cơ bản đến nâng cao" tại đây