blog.dopana

Back

Unit test, integration test, E2E test là nền tảng — nhưng đó mới chỉ là khởi đầu.

Khi ứng dụng phát triển, bạn sẽ đối mặt với những câu hỏi khó hơn:

  • Làm sao đảm bảo API contract giữa microservices không bị phá vỡ?
  • Làm sao phát hiện UI bị vỡ layout sau khi refactor CSS?
  • Làm sao tìm ra edge case mà không cần nghĩ trước?
  • Làm sao biết test suite của bạn có chất lượng?

Bài viết này giới thiệu 6 chiến lược testing nâng cao — mỗi strategy giải quyết một vấn đề mà unit test truyền thống không làm tốt.

1. Contract Testing — Bảo Vệ API Boundary#

Vấn đề#

Microservices giao tiếp qua API. Một service thay đổi response format → service khác parse sai → sập.

Integration test có thể phát hiện, nhưng chạy chậm và cần infrastructure. E2E test phát hiện được nhưng lại quá muộn trong pipeline.

Giải pháp: Consumer-Driven Contract (CDC)#

Contract testing kiểm tra rằng consumer và provider thống nhất về format dữ liệu — mà không cần chạy service thật.

Ở phía provider:

// Provider: Tôi đảm bảo API thỏa mãn contract
// Chạy Pact verification trong CI

func Test_UserHandler_VerifyPact(t *testing.T) {
    pact.Verify(t, func() error {
        // Start server thật hoặc in-memory
        router := setupRouter()
        return http.ListenAndServe(":8000", router)
    })
}
go

Khi nào dùng#

Nên dùngKhông nên dùng
Microservices của team khácMonolith
API public/third-partyInternal function call
Nhiều consumer khác nhau1-2 services, ít thay đổi

Công cụ#

  • Pact — Chuẩn thực tế cho CDC, hỗ trợ Go, JS, Rust, Python, Java
  • Spring Cloud Contract — Cho JVM ecosystem
  • Hoverfly — Mock HTTP dạng contract

2. Property-Based Testing — Để Machine Tìm Bug#

Vấn đề#

Bạn viết unit test cho 5-10 trường hợp. Nhưng production có hàng nghìn biến thể. Edge case nào đó bạn không nghĩ tới → bug.

Giải pháp: Kiểm tra tính chất, không kiểm tra giá trị#

Thay vì assert giá trị cụ thể, bạn định nghĩa property (tính chất) mà function phải thỏa mãn với mọi input hợp lệ.

Framework sẽ sinh hàng loạt input ngẫu nhiên để kiểm tra.

import fc from 'fast-check'

// Property: reverse(reverse(arr)) === arr
test('reverse is idempotent', () => {
  fc.assert(
    fc.property(fc.array(fc.anything()), (arr) => {
      expect(arr.reverse().reverse()).toEqual(arr)
    })
  )
})
typescript
// Property: parse và stringify luôn bảo toàn dữ liệu
test('parse and stringify are inverses', () => {
  fc.assert(
    fc.property(fc.object(), (obj) => {
      const json = JSON.stringify(obj)
      const parsed = JSON.parse(json)
      expect(parsed).toEqual(obj)
    })
  )
})
typescript
// Property: sort luôn trả về mảng có thứ tự
test('sort returns ordered array', () => {
  fc.assert(
    fc.property(fc.array(fc.integer()), (arr) => {
      const sorted = [...arr].sort((a, b) => a - b)
      for (let i = 1; i < sorted.length; i++) {
        expect(sorted[i - 1]).toBeLessThanOrEqual(sorted[i])
      }
    })
  )
})
typescript

Mỗi lần chạy, fast-check sinh 100 input ngẫu nhiên. Nếu có lỗi, nó thu nhỏ (shrink) input về trường hợp tối thiểu để reproduce:

Error: Property failed after 42 tests
Counterexample: [NaN, 1, -0, 1e308]
Shrink result: [NaN]
plaintext

Khi nào dùng#

  • Function thuần túy (pure) — cùng input → cùng output
  • Thuật toán (sort, search, crypto, encode/decode)
  • Parser, validator, serializer
  • Business logic có nhiều edge case

Công cụ#

  • fast-check (JS/TS) — Sinh test, shrink, custom arbitraries
  • QuickCheck (Haskell) — Ông tổ của property-based testing
  • Proptest (Rust) — Mạnh mẽ, integration với cargo-test
  • Hypothesis (Python) — Tính năng tương đương fast-check
  • fs2 (Go) — Property testing cho Go, nhẹ và đơn giản

3. Visual Regression Testing — Mắt Thứ Ba Cho UI#

Vấn đề#

Bạn sửa CSS, thay đổi component — layout, màu sắc, spacing, font bị vỡ ở đâu đó. Unit test không phát hiện. Manual testing bỏ sót.

Giải pháp: So sánh ảnh UI tự động#

Chụp screenshot, so sánh pixel-by-pixel với baseline, báo diff nếu khác.

Khi có thay đổi UI:

# Chạy test
npx playwright test

# Nếu snapshot fail, kiểm tra diff
npx playwright show-report
# → Xem ảnh: baseline | actual | diff
bash

Quy trình làm việc với snapshot#

Feature branch

Chạy visual test

Snapshot fail (có sự khác biệt)

┌─ Nếu là thay đổi mong muốn ───────┐
│ Update snapshot → commit          │
└───────────────────────────────────┘
┌─ Nếu là lỗi ──────────────────────┐
│ Fix code → chạy lại test          │
└───────────────────────────────────┘
text

Khi nào dùng#

  • Component library / Design system
  • Trang có layout phức tạp (dashboard, bảng biểu)
  • Theme / Dark mode support
  • Cross-browser testing

Công cụ#

  • Playwright — toHaveScreenshot() built-in, mạnh, miễn phí
  • Chromatic — Cho Storybook, cloud-hosted, review UI trên PR
  • Percy — BrowserStack, tích hợp CI, quản lý snapshot
  • Loki — Docker-based, so sánh Chrome vs Chrome
  • BackstopJS — Config bằng JSON, hỗ trợ đa trình duyệt

[!NOTE] Snapshot UI dễ hỏng (font, OS, GPU rendering khác nhau). Dùng Docker hoặc CI runner cố định để giảm false positive.

4. Mutation Testing — Test Cho Test Của Bạn#

Vấn đề#

95% coverage — nhưng bạn có chắc test suite của mình thực sự phát hiện lỗi? Hay chỉ chạy qua cho có?

Giải pháp: Đột biến code, xem test có fail không#

Mutation testing tự động thay đổi (đột biến) code của bạn — và xem test suite có bắt được không.

// Code gốc
function isEven(n: number): boolean {
  return n % 2 === 0
}

// Mutation 1: Đổi === thành !==
function isEven(n: number): boolean {
  return n % 2 !== 0  // ← test có fail không?
}

// Mutation 2: Đổi % 2 thành % 3
// Mutation 3: return true thay vì tính toán
// ...
typescript

Kết quả:

 mutants: 42
 killed:  38  (90.5%)  ✓ Test phát hiện đột biến
 survived: 4  (9.5%)   ✗ Test không phát hiện
plaintext

Nếu test của bạn không bắt được mutation nào — test đó đang chạy cho có, không kiểm tra gì cả.

Khi nào dùng#

  • Khi viết critical business logic
  • Khi refactor và muốn đảm bảo test coverage có chất lượng
  • Audit test suite định kỳ

Công cụ#

  • Stryker (JS/TS) — Hỗ trợ Vitest, Jest, Mocha. Có HTML report chi tiết
  • PIT (Java) — Chuẩn mực cho mutation testing trong JVM
  • Mutmut (Python) — Đơn giản, dễ dùng
  • cargo-mutants (Rust) — Mutation testing cho Rust
# Stryker với Vitest
npx stryker run

# Output:
# ---------|---------|---------|---------|
# File | % score | # killed| # survived |
# ---------|---------|---------|---------|
# user.ts | 92.31 | 12 | 1 |
# payment | 85.71 | 6 | 1 |
# order | 100.00 | 8 | 0 |
# ---------|---------|---------|---------|
bash

[!TIP] Mutation testing chạy chậm (mỗi mutation = 1 lần chạy test). Chỉ chạy trên module critical, không chạy toàn bộ project mỗi commit. Chạy nightly hoặc pre-release.

5. Fuzz Testing — Tìm Lỗi Bảo Mật & Crash#

Vấn đề#

Bạn test với input “hợp lý”. Hacker gửi input kỳ dị — overflow, injection, crash — bạn không test.

Giải pháp: Bắn dữ liệu ngẫu nhiên vào ứng dụng#

Fuzz testing (fuzzing) tự động sinh input ngẫu nhiên, sai lệch, hoặc có cấu trúc để tìm crash, memory leak, và lỗi bảo mật.

go test -fuzz=FuzzParsePhoneNumber -fuzztime=30s
bash

Output:

=== FUZZ  FuzzParsePhoneNumber
    crash detected: panic in ParsePhoneNumber
    input: "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00..."
plaintext

Rust:#

// Cargo-fuzz hoặc libfuzzer
use arbitrary::Arbitrary;

#[derive(Arbitrary, Debug)]
struct FuzzInput {
    data: Vec<u8>,
    config: u8,
}

fn fuzz_parse(input: FuzzInput) {
    // Fuzzer sẽ thử mọi biến thể của FuzzInput
    let _ = parse_data(&input.data, input.config);
}
rust

Khi nào dùng#

  • Parser (JSON, XML, CSV, URL, phone, email)
  • Network protocol handler
  • File format processor
  • Authentication / Authorization
  • Input validation layer

Công cụ#

  • go-fuzz / native Go fuzzing — Built-in từ Go 1.18
  • cargo-fuzz / libfuzzer (Rust) — LLVM-based, cực nhanh
  • Jazzer (Java) — Fuzzing cho JVM, dùng libfuzzer
  • AFL — General-purpose fuzzer, dùng được với nhiều ngôn ngữ

6. Load Testing — Biết Giới Hạn Của Hệ Thống#

Vấn đề#

App chạy tốt ở dev và staging. Lên production, 1000 user cùng request → chết, timeout, OOM.

Giải pháp: Mô phỏng tải thực tế#

Load testing gửi request với số lượng user, request rate, và pattern giống production — để tìm bottleneck trước khi user thật bị ảnh hưởng.

k6 run load-test.js
bash

Smoke → Stress → Spike → Soak#

LoạiMục đíchTải
Smoke TestHệ thống có chạy?1-5 user
Load TestHệ thống chịu được tải bình thường?~expected peak
Stress TestGiới hạn ở đâu?expected → 2x → 5x
Spike TestChịu surge đột ngột?0 → 10x instant
Soak TestChạy ổn định 24h?~expected trong 8-24h

Khi nào dùng#

  • API endpoint quan trọng (login, payment, search)
  • Before production release
  • Sau khi thay đổi kiến trúc (DB, cache, queue)
  • Hàng tháng / hàng quý

Công cụ#

  • k6 (Grafana) — Script bằng JS, metric built-in, tích hợp CI mạnh
  • autocannon (Node.js) — Nhẹ, CLI đơn giản, HTTP benchmark
  • wrk / wrk2 — Benchmark HTTP, C-based, cực nhanh
  • Locust (Python) — Viết test bằng Python, UI real-time
  • Gatling (Scala) — Mạnh mẽ, có HTML report, JVM ecosystem
  • Vegeta (Go) — CLI đơn giản, HTTP load testing

Tổng Kết: Khi Nào Dùng Strategy Nào?#

Không project nào cần tất cả. Hãy chọn dựa vào:

  1. Loại ứng dụng — API-heavy → Contract + Load. UI-heavy → Visual
  2. Giai đoạn — Mới bắt đầu → Unit + Integration. Đã ổn định → Property + Fuzz
  3. Rủi ro — Xử lý tiền → Fuzz. Design system → Visual. Public API → Contract

Testing không phải là checkbox. Mỗi strategy là một công cụ — hiểu vấn đề rồi mới chọn công cụ phù hợp.

Cheatsheet: 6 Strategies Trong 1 Bảng#

StrategyPhát hiệnChi phíTốc độCông cụ chính
ContractAPI boundary breakThấpNhanhPact
PropertyEdge case logicTrung bìnhTrung bìnhfast-check
VisualUI regressionTrung bìnhTrung bìnhPlaywright
MutationTest qualityCaoChậmStryker
FuzzCrash, securityThấpChậmgo-fuzz
LoadPerformance bottleneckCaoNhanhk6

Unit test là chân kiềng đầu tiên. Nhưng để ứng dụng thực sự vững chắc — trước những thay đổi API, UI vô tình bị vỡ, edge case không ai nghĩ tới, hay traffic spike bất ngờ — bạn cần nhiều hơn thế.

Hãy chọn 1-2 strategies phù hợp với vấn đề bạn đang gặp nhất, tích hợp vào CI, và nâng dần khi ứng dụng phát triển.

Tài liệu tham khảo#