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.
// Consumer: Tôi mong đợi response như thế này
// Dùng Pact framework
func Test_UserService_GetUser(t *testing.T) {
// Định nghĩa contract: consumer mong đợi
pact.
AddInteraction().
Given("User exists").
UponReceiving("A request for user 1").
WithRequest(request{
Method: "GET",
Path: "/users/1",
}).
WillRespondWith(response{
Status: 200,
Body: map[string]any{"id": 1, "name": "Alice"},
})
// Test consumer với mock provider
// Nếu consumer parse sai → test fail
}goỞ 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)
})
}goKhi nào dùng#
| Nên dùng | Không nên dùng |
|---|---|
| Microservices của team khác | Monolith |
| API public/third-party | Internal function call |
| Nhiều consumer khác nhau | 1-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])
}
})
)
})typescriptMỗ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]plaintextKhi 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.
// Playwright Visual Comparison
import { test, expect } from '@playwright/test'
test('homepage matches snapshot', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100, // cho phép sai khác tối đa 100px
threshold: 0.2, // pixel diff threshold
})
})
test('login form renders correctly', async ({ page }) => {
await page.goto('/login')
const form = page.locator('.login-form')
await expect(form).toHaveScreenshot('login-form.png')
})typescriptKhi 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 | diffbashQuy 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 │
└───────────────────────────────────┘textKhi 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
// ...typescriptKế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ệnplaintextNế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 fuzz testing
func FuzzParsePhoneNumber(f *testing.F) {
// Seed corpus: một vài input mẫu
f.Add("+84123456789")
f.Add("0123 456 789")
f.Add("(+84) 123-456-789")
f.Fuzz(func(t *testing.T, input string) {
result, err := ParsePhoneNumber(input)
if err != nil {
return // lỗi hợp lệ
}
// Nếu parse thành công, result phải hợp lệ
if len(result) < 10 {
t.Errorf("phone too short: %s → %s", input, result)
}
})
}gogo test -fuzz=FuzzParsePhoneNumber -fuzztime=30sbashOutput:
=== FUZZ FuzzParsePhoneNumber
crash detected: panic in ParsePhoneNumber
input: "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00..."plaintextRust:#
// 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);
}rustKhi 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 script
import http from 'k6/http'
import { check, sleep } from 'k6'
export const options = {
stages: [
{ duration: '2m', target: 100 }, // ramp up
{ duration: '5m', target: 100 }, // sustain
{ duration: '2m', target: 200 }, // spike
{ duration: '3m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% request < 500ms
http_req_failed: ['rate<0.01'], // < 1% lỗi
},
}
export default function () {
const res = http.get('https://api.example.com/users')
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 300ms': (r) => r.timings.duration < 300,
})
sleep(1)
}javascriptk6 run load-test.jsbashSmoke → Stress → Spike → Soak#
| Loại | Mục đích | Tải |
|---|---|---|
| Smoke Test | Hệ thống có chạy? | 1-5 user |
| Load Test | Hệ thống chịu được tải bình thường? | ~expected peak |
| Stress Test | Giới hạn ở đâu? | expected → 2x → 5x |
| Spike Test | Chịu surge đột ngột? | 0 → 10x instant |
| Soak Test | Chạ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?#
┌────────────────────────────────────────────────────────┐
│ TESTING STRATEGY MAP │
├────────────────────────────────────────────────────────┤
│ │
│ Contract ←─── API change nhiều, nhiều consumer │
│ │
│ Property ←─── Logic phức tạp, nhiều edge case │
│ │
│ Visual ←─── UI/component library, cross-browser │
│ │
│ Mutation ←─── Audit chất lượng test suite │
│ │
│ Fuzz ←─── Security-critical, parser, validator │
│ │
│ Load ←─── API endpoint, pre-release │
│ │
└────────────────────────────────────────────────────────┘textKhông project nào cần tất cả. Hãy chọn dựa vào:
- Loại ứng dụng — API-heavy → Contract + Load. UI-heavy → Visual
- Giai đoạn — Mới bắt đầu → Unit + Integration. Đã ổn định → Property + Fuzz
- 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#
| Strategy | Phát hiện | Chi phí | Tốc độ | Công cụ chính |
|---|---|---|---|---|
| Contract | API boundary break | Thấp | Nhanh | Pact |
| Property | Edge case logic | Trung bình | Trung bình | fast-check |
| Visual | UI regression | Trung bình | Trung bình | Playwright |
| Mutation | Test quality | Cao | Chậm | Stryker |
| Fuzz | Crash, security | Thấp | Chậm | go-fuzz |
| Load | Performance bottleneck | Cao | Nhanh | k6 |
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.