Giới thiệu

Trong môi trường phát triển phần mềm hiện đại, kiểm thử tự động không còn là lựa chọn phụ trợ mà đã trở thành yêu cầu bắt buộc để duy trì chất lượng code, giảm thiểu lỗi runtime và tăng tốc độ đưa sản phẩm ra thị trường. Đối với các dự án TypeScript, việc thiết lập một hệ thống kiểm thử toàn diện đòi hỏi phải cân nhắc cả unit testend‑to‑end (E2E) test. Bài viết này sẽ phân tích chi tiết cách xây dựng môi trường kiểm thử tự động bằng Jest cho unit test và Playwright cho E2E, đồng thời đưa ra các best practices giúp dự án TypeScript của bạn luôn ổn định và dễ bảo trì.

Lý do cần kiểm thử tự động trong dự án TypeScript

TypeScript cung cấp khả năng type checking ở thời gian biên dịch, nhưng không thể phát hiện mọi lỗi logic hay các vấn đề tương tác giữa các module. Các lỗi thường gặp bao gồm:

  • Thao tác bất đồng bộ không được xử lý đúng cách, dẫn đến race condition.
  • Thay đổi API mà không cập nhật các consumer, gây runtime error.
  • Tham số đầu vào không được xác thực, gây lỗi bảo mật.

Kiểm thử tự động giúp phát hiện sớm những vấn đề này, đồng thời tạo ra tài liệu sống (living documentation) cho các hàm và component.

Cấu trúc thư mục chuẩn cho kiểm thử TypeScript

Một cấu trúc thư mục hợp lý giúp tách biệt code production và test, đồng thời hỗ trợ các công cụ build như tsc hoặc babel. Dưới đây là một ví dụ thực tế:

src/
├── api/
│   ├── userController.ts
│   └── productService.ts
├── components/
│   └── Button.tsx
└── utils/
    └── validator.ts

tests/
├── unit/
│   ├── api/userController.test.ts
│   └── utils/validator.test.ts
└── e2e/
    └── login.flow.spec.ts

Thư mục tests/unit chứa các test mức độ hàm (unit test), còn tests/e2e chứa các kịch bản kiểm thử toàn bộ luồng người dùng.

Thiết lập Jest cho unit test TypeScript

Cài đặt các package cần thiết

Chạy lệnh sau để cài đặt Jest, TypeScript preset và các công cụ hỗ trợ:

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

Cấu hình jest.config.js

File cấu hình dưới đây sử dụng ts-jest để biên dịch TypeScript trong quá trình chạy test:

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/tests/unit'],
  moduleFileExtensions: ['ts', 'js', 'json', 'node'],
  transform: {
    '^.+\\.ts$': 'ts-jest'
  },
  collectCoverage: true,
  coverageDirectory: '<rootDir>/coverage',
  coverageReporters: ['text', 'lcov']
};

Ví dụ unit test cho hàm validateEmail

Giả sử chúng ta có một hàm trong src/utils/validator.ts:

export function validateEmail(email: string): boolean {
  const re = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
  return re.test(email);
}

Unit test tương ứng:

import { validateEmail } from '../../src/utils/validator';

describe('validateEmail', () => {
  it('should return true for a valid email', () => {
    expect(validateEmail('[email protected]')).toBe(true);
  });

  it('should return false for an invalid email', () => {
    expect(validateEmail('invalid-email')).toBe(false);
  });
});

Khi chạy npm test, Jest sẽ biên dịch file .ts và thực thi các khối describe/it để xác nhận hành vi.

Thiết lập Playwright cho E2E test

Cài đặt Playwright

Playwright hỗ trợ đa trình duyệt (Chromium, Firefox, WebKit) và tích hợp tốt với TypeScript. Cài đặt bằng lệnh:

npm install --save-dev @playwright/test typescript

Cấu hình playwright.config.ts

File cấu hình dưới đây định nghĩa môi trường test, timeout và các reporter:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: 'tests/e2e',
  timeout: 30_000,
  expect: {
    timeout: 5_000
  },
  reporter: [['list'], ['html', { open: 'never' }]],
  use: {
    headless: true,
    viewport: { width: 1280, height: 720 },
    ignoreHTTPSErrors: true,
    video: 'retain-on-failure'
  },
  projects: [
    { name: 'Chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'Firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'WebKit', use: { ...devices['Desktop Safari'] } }
  ]
});

Ví dụ kịch bản đăng nhập

Giả sử ứng dụng có trang /login với form gồm emailpassword. Kịch bản kiểm thử:

import { test, expect } from '@playwright/test';

test('đăng nhập thành công với thông tin hợp lệ', async ({ page }) => {
  await page.goto('http://localhost:3000/login');
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('input[name="password"]', 'Password123');
  await page.click('button[type="submit"]');

  // Kiểm tra chuyển hướng tới dashboard
  await expect(page).toURL('http://localhost:3000/dashboard');
  await expect(page.locator('h1')).toHaveText('Chào mừng');
});

test('hiển thị lỗi khi mật khẩu sai', async ({ page }) => {
  await page.goto('http://localhost:3000/login');
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('input[name="password"]', 'WrongPass');
  await page.click('button[type="submit"]');

  const error = page.locator('.error-message');
  await expect(error).toBeVisible();
  await expect(error).toHaveText('Mật khẩu không đúng');
});

Playwright sẽ tự động khởi tạo trình duyệt, thực thi các hành động và báo cáo kết quả chi tiết.

Best practices khi kết hợp Jest và Playwright trong dự án TypeScript

  • Đặt mục tiêu rõ ràng: Unit test tập trung vào logic thuần, còn E2E test kiểm tra luồng người dùng và tích hợp.
  • Sử dụng fixtures trong Playwright để tái sử dụng các thao tác khởi tạo (đăng nhập, tạo dữ liệu mẫu).
  • Mock các phụ thuộc ngoài trong unit test bằng jest.mock() để tránh gọi API thực tế.
  • Chạy test song song bằng cách cấu hình maxWorkers trong Jest và tận dụng đa trình duyệt trong Playwright.
  • Thu thập coverage từ Jest và tích hợp báo cáo coverage vào CI để giám sát mức độ bao phủ.
  • Lưu trữ artefacts (video, screenshot) từ Playwright trong pipeline CI để hỗ trợ debug.

Tích hợp kiểm thử vào CI/CD

Trong môi trường CI (GitHub Actions, GitLab CI, Azure Pipelines), chúng ta nên thực hiện các bước sau:

  1. Cài đặt các dependency và biên dịch TypeScript.
  2. Chạy npm test để thực hiện Jest unit test.
  3. Khởi động môi trường tạm thời (Docker compose) nếu cần API backend.
  4. Chạy npx playwright test để thực hiện E2E test.
  5. Upload báo cáo coverage và artefacts (video, screenshot) lên hệ thống lưu trữ.

Dưới đây là một snippet cấu hình GitHub Actions:

name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      db:
        image: postgres:13
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        ports: ['5432:5432']
        options: >-
          --health-cmd "pg_isready -U test"
    steps:
      - uses: actions/checkout@v3
      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run build
      - name: Run unit tests
        run: npm test
      - name: Run E2E tests with Playwright
        run: npx playwright test
      - name: Upload Playwright Artifacts
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-results
          path: test-results/**

Phân tích hiệu năng và tối ưu hoá

Khi dự án lớn, thời gian chạy test có thể kéo dài. Một số chiến lược giảm thiểu thời gian:

  • Test sharding: Chia các test suite thành các phần nhỏ và chạy song song trên nhiều runner.
  • Cache node_modules trong CI để tránh tải lại dependencies.
  • Chỉ chạy E2E test trên các pull request quan trọng (ví dụ: khi thay đổi UI).
  • Sử dụng --detectOpenHandles trong Jest để phát hiện các handle chưa được đóng, tránh test treo.

Những lỗi thường gặp và cách khắc phục

1. Lỗi Cannot find module khi import TypeScript trong test

Nguyên nhân thường do tsconfig.json không đồng bộ với jest.config.js. Đảm bảo moduleNameMapper trong Jest ánh xạ đúng các alias:

moduleNameMapper: {
  '^@src/(.*)$': '<rootDir>/src/$1'
}

2. Playwright không thể kết nối tới server trong CI

Kiểm tra rằng server đã được khởi động và lắng nghe trên đúng cổng. Thêm bước wait-on để chờ server sẵn sàng:

npm install -g wait-on
wait-on http://localhost:3000 && npx playwright test

3. Test chậm do tạo quá nhiều đối tượng DOM

Sử dụng page.context().clearCookies()page.close() sau mỗi test để giải phóng tài nguyên.

Kết luận

Việc xây dựng một hệ thống kiểm thử tự động cho dự án TypeScript không chỉ giúp giảm thiểu lỗi mà còn nâng cao năng suất phát triển. Bằng cách kết hợp Jest cho unit test và Playwright cho E2E test, bạn sẽ có được một pipeline kiểm thử toàn diện, dễ mở rộng và tích hợp tốt với CI/CD. Nếu muốn nâng cao kỹ năng TypeScript và áp dụng các kiến thức trên một cách bài bản, Tham khảo khóa học "Lập trình TypeScript từ cơ bản đến nâng cao" tại đây.