Flaky test — test không ổn định, hôm nay pass hôm sau fail dù không ai đụng vào code — là một trong những nỗi nhức đầu tốn thời gian nhất của bất kỳ team QA nào. Cái khó không nằm ở việc biết test đang flaky, mà nằm ở việc tìm ra root cause (nguyên nhân gốc) thực sự đằng sau nó, giữa hàng chục nguyên nhân khả nghi: race condition, network chập chờn, order dependency, timeout quá chặt... Phần lớn thời gian debug flaky test bị lãng phí vào việc rerun test 10 lần để "xem nó có fail lại không" — một cách tiếp cận mang tính may rủi hơn là khoa học.
Trong bài này, chúng ta sẽ dùng AI (LLM) như một công cụ phân tích dữ liệu: đưa cho nó lịch sử chạy test, log CI, và source code liên quan, để nó giúp bạn hình thành giả thuyết (hypothesis), tự thiết kế thực nghiệm kiểm chứng, và cuối cùng đưa ra quyết định fix hay quarantine (cách ly) test đó. Đây là kỹ năng bắt buộc với QA Mid/Senior khi làm việc trong pipeline CI/CD có hàng nghìn test chạy mỗi ngày.
Những nguyên nhân phổ biến nhất gây ra flaky test là gì, và AI phát hiện chúng như thế nào?
Trước khi giao việc cho AI, bạn cần hiểu rõ "bản đồ nguyên nhân" mà AI sẽ dựa vào để suy luận — nếu không, bạn sẽ không biết đánh giá AI đang gợi ý đúng hay đang "bịa" (hallucinate) một lý do nghe hợp lý nhưng sai.
Các nhóm nguyên nhân phổ biến nhất trong thực tế:
- Race condition (tình trạng tranh chấp thứ tự thực thi giữa các tác vụ bất đồng bộ): assertion chạy trước khi state thực sự được cập nhật xong.
- Order dependency / test pollution: test A để lại state (DB row, biến global, cookie, cache) mà test B vô tình phụ thuộc vào, nên B chỉ pass khi A chạy trước.
- Timing/timeout sai cách: dùng
sleep()cứng, hoặc timeout quá ngắn so với thời gian xử lý thực tế trên môi trường CI (thường chậm hơn máy local). - Shared mutable state giữa các test chạy parallel: cùng ghi vào một user, một record, một port.
- Network flakiness: DNS chập chờn, rate-limit từ third-party API, packet loss trong container network.
- Environment/infra: container bị throttle CPU, disk I/O chậm bất thường, clock skew giữa các node.
- Non-determinism trong code:
Date.now(),Math.random(), thứ tự key trong object/Map không được đảm bảo. - Async waiting sai: chờ theo
setTimeoutcố định thay vì chờ theo điều kiện (polling/waitFor), hoặc thiếu awaiting một promise.
AI phát hiện các nguyên nhân này bằng cách làm điều mà con người làm chậm và dễ bỏ sót: đối chiếu pattern trên diện rộng. Khi bạn feed cho LLM nhiều lần chạy (cả pass và fail) kèm metadata (thời gian, worker, thứ tự test, log lỗi), nó có thể phát hiện tương quan như "lỗi luôn xảy ra khi duration > 300ms" hoặc "lỗi chỉ xảy ra khi test X chạy ngay trước" — những thứ mà việc đọc từng log riêng lẻ khó thấy ra.
Bảng phân loại nguyên nhân flaky test (taxonomy)
Dùng bảng phân loại này làm "từ điển tra cứu" khi review kết luận của AI:
| Nhóm nguyên nhân | Dấu hiệu nhận biết trong log | Ví dụ error message | Hướng fix điển hình |
|---|---|---|---|
| Race condition | Fail ngẫu nhiên, không theo thứ tự cố định, thường liên quan async | Timed out waiting for element to have text |
Chờ theo điều kiện thay vì thời gian; dùng waitFor/polling |
| Order dependency | Chỉ fail khi chạy full suite, pass khi chạy riêng lẻ | Expected cart to be empty but found 2 items |
Reset state trước mỗi test; tránh shared fixture |
| Timing/Timeout | Fail nhiều hơn trên CI so với local, tăng khi CI load cao | Timeout 5000ms exceeded |
Tăng timeout hợp lý + chờ theo điều kiện, không chỉ tăng số |
| Shared state | Nhiều test cùng fail cụm khi chạy parallel | Unique constraint violation |
Tách dữ liệu test theo test/worker (test data isolation) |
| Network flakiness | Lỗi liên quan socket/DNS/HTTP 5xx ngẫu nhiên | net::ERR_CONNECTION_RESET |
Retry có giới hạn, mock network trong test, alert riêng cho hạ tầng |
| Non-deterministic code | Kết quả khác nhau dù input giống nhau tuyệt đối | Assertion mismatch không rõ nguyên nhân | Seed random, freeze time trong test |
Mẹo: Trước khi hỏi AI "tại sao test này flaky", hãy tự dán nhãn tạm ứng viên nguyên nhân dựa trên bảng taxonomy này. Việc này giúp bạn viết prompt có định hướng, và quan trọng hơn là giúp bạn phản biện được khi AI đưa ra kết luận không khớp với bằng chứng.
Test Code
Xuyên suốt bài viết, ta sẽ dùng một test E2E thực tế làm ví dụ chạy xuyên các phần: test kiểm tra tổng tiền giỏ hàng cập nhật sau khi thêm sản phẩm, viết bằng Playwright.
// tests/e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';
test('should update cart total after adding item', async ({ page }) => {
await page.goto('/products/sku-2291');
await page.click('[data-testid="add-to-cart"]');
await expect(page.locator('[data-testid="cart-total"]')).toHaveText('$129.00');
});
Mẹo: Khi chuẩn bị code để đưa cho AI phân tích, đừng cắt bớt test cho "gọn" — giữ nguyên locator, giữ nguyên assertion gốc kể cả khi nó trông "quá đơn giản để sai". AI cần thấy đúng những gì CI thấy, không phải bản đã được bạn diễn giải lại.
What This Test Covers
Test này verify luồng: user bấm "Add to cart" → hệ thống gọi API tính lại tổng tiền giỏ hàng ở server → UI cập nhật số hiển thị ở [data-testid="cart-total"]. Đây là một luồng có tính bất đồng bộ hai tầng: (1) network call cập nhật dữ liệu, (2) UI re-render dựa trên dữ liệu mới. Bất kỳ khoảng hở giữa hai tầng này đều là ứng viên tiềm năng cho race condition — và chính là điểm nghi vấn số một khi test này bắt đầu flaky.
Mẹo: Luôn viết rõ phần "What This Test Covers" bằng ngôn ngữ nghiệp vụ (không chỉ mô tả code) khi đưa cho AI. Nó giúp AI phân biệt được đâu là hành vi đúng theo thiết kế và đâu là bug thực sự, tránh việc AI đề xuất "sửa" một hành vi vốn đã đúng.
Cách đưa lịch sử flaky test và log CI cho AI để nhận diện pattern?
Sai lầm phổ biến nhất khi dùng AI debug flaky test: chỉ dán một log lỗi rồi hỏi "tại sao test này fail?". Với đúng một mẫu dữ liệu, AI (và cả con người) chỉ có thể đoán, không thể phân tích pattern. Bạn cần cung cấp lịch sử nhiều lần chạy — cả fail và pass — kèm metadata đủ để AI tìm tương quan.
Ví dụ prompt hiệu quả:
Bạn là một Site Reliability/QA engineer chuyên debug flaky test.
Dưới đây là lịch sử 12 lần chạy gần nhất của test
"should update cart total after adding item" (tests/e2e/checkout.spec.ts:14),
gồm cả run pass và fail, kèm log, thời gian chạy, worker, và test chạy ngay trước nó.
[dán Run History + log từng run]
Yêu cầu:
1. Liệt kê tất cả pattern/tương quan bạn quan sát được giữa các run FAIL
(thời gian, worker, duration, test đứng trước, error message).
2. Với mỗi pattern, đưa ra 1 giả thuyết nguyên nhân gốc cụ thể, kèm mức độ tự tin (%).
3. Chỉ rõ bằng chứng nào trong log ỦNG HỘ và bằng chứng nào MÂU THUẪN với mỗi giả thuyết.
4. Đề xuất 1 thực nghiệm tối thiểu để kiểm chứng giả thuyết có độ tự tin cao nhất.
Mẹo: Luôn yêu cầu AI chỉ ra "bằng chứng mâu thuẫn" (contradicting evidence), không chỉ bằng chứng ủng hộ. LLM có xu hướng chọn giả thuyết đầu tiên nghe hợp lý rồi tìm cách biện minh cho nó (confirmation bias) — ép nó tự phản biện giúp lộ ra giả thuyết yếu sớm hơn.
Chuẩn hoá lịch sử lỗi (failure history) trước khi đưa cho AI phân tích
Dữ liệu rời rạc, không đồng nhất format là lý do số một khiến AI phân tích sai. Hãy chuẩn hoá về một schema cố định, ví dụ JSON:
{
"test_name": "should update cart total after adding item",
"file": "tests/e2e/checkout.spec.ts",
"run_id": "#1234",
"timestamp_utc": "2024-03-15T02:14:00Z",
"status": "fail",
"duration_ms": 5041,
"ci_worker": "worker-3",
"shard": "2/4",
"order_index": 47,
"retry_count": 0,
"browser": "chromium-122",
"preceding_test": "should apply discount code",
"git_sha": "a1b2c3d",
"error_message": "Timeout 5000ms exceeded waiting for locator to have text.",
"stack_trace_hash": "sha1:9f8e7d"
}
Trường stack_trace_hash đặc biệt hữu ích: dùng nó để nhóm các run fail có cùng "chữ ký lỗi" thành cụm, trước khi hỏi AI — giúp AI không phải tự đoán run nào cùng loại lỗi với run nào.
Mẹo: Nếu hệ thống CI của bạn (GitHub Actions, GitLab CI, Jenkins) không có sẵn các trường này, hãy viết một script nhỏ trích xuất từ log thô sang JSON schema trên trước khi feed cho AI — đầu tư 30 phút viết script tiết kiệm hàng giờ AI đoán sai vì dữ liệu lộn xộn.
Test: [test name and file path]
should update cart total after adding item—tests/e2e/checkout.spec.ts:14
Mẹo: Luôn ghi kèm số dòng (:14) chứ không chỉ tên file. Khi AI cần đối chiếu với source code ở bước xác nhận root cause về sau, số dòng giúp nó khoanh vùng đúng đoạn code ngay lần đầu, tránh việc nó phân tích nhầm một hàm khác cùng tên.
Run History
| Run ID | Thời gian (UTC) | Trạng thái | Duration | Worker | Order index | Test đứng trước |
|---|---|---|---|---|---|---|
| #1197 | 2024-03-12 03:02 | pass | 812ms | worker-1 | 12 | should show product price |
| #1210 | 2024-03-13 01:47 | pass | 795ms | worker-2 | 30 | should apply discount code |
| #1234 | 2024-03-15 02:14 | fail | 5041ms | worker-3 | 47 | should apply discount code |
| #1251 | 2024-03-16 14:38 | fail | 5038ms | worker-1 | 8 | should remove item from cart |
| #1268 | 2024-03-17 04:12 | pass | 803ms | worker-2 | 22 | should show product price |
| #1287 | 2024-03-18 09:22 | fail | 3120ms | worker-4 | 15 | should show product price |
| #1290 | 2024-03-18 11:05 | pass | 1399ms | worker-3 | 47 | should apply discount code |
| #1301 | 2024-03-20 17:45 | fail | 5052ms | worker-2 | 48 | should apply discount code |
| #1319 | 2024-03-21 06:30 | pass | 780ms | worker-1 | 10 | should show product price |
Mẹo: Trình bày Run History dạng bảng (không phải văn xuôi) khi đưa cho AI. Format bảng giúp LLM giữ đúng alignment giữa các cột số liệu — dán dạng câu văn thường khiến model "trộn" nhầm giá trị duration của run này sang run khác khi suy luận.
Phân tích log CI để lấy context cho lỗi flaky test
Bảng Run History cho bạn khi nào fail, nhưng log CI thô mới cho bạn biết tại sao. Với mỗi run fail, hãy trích: stdout/stderr đầy đủ của assertion, console log của browser (nếu là E2E), trace/HAR network nếu có, và ảnh chụp/screenshot recording. Dưới đây là 4 run fail và 1 run pass để đối chiếu — chú ý cách mỗi run kể một câu chuyện khác nhau, dù cùng một test.
Failing Run 1 (Run ID: #1234, 2024-03-15 02:14 UTC)
[worker-3] tests/e2e/checkout.spec.ts:14
should update cart total after adding item
Error: Timeout 5000ms exceeded.
Locator: [data-testid="cart-total"]
Expected string: "$129.00"
Received string: "$99.00"
Call log:
- expect.toHaveText with timeout 5000ms
- retrying every 100ms
- locator resolved to <span data-testid="cart-total">$99.00</span>
- unexpected value "$99.00"
Network trace: POST /api/cart/recalculate → 200 OK @ 5187ms (sau timeout)
Preceding test: should apply discount code (order_index 46)
Mẹo: Ghi rõ mốc thời gian network call hoàn tất so với mốc timeout của assertion (ở đây: API trả về ở 5187ms, timeout của assertion là 5000ms). Đây là chi tiết định lượng quan trọng nhất để AI phân biệt "chậm một chút" (fix bằng tăng timeout) với "race condition thật" (fix bằng đổi cơ chế chờ).
Failing Run 2 (Run ID: #1251, 2024-03-16 14:38 UTC)
[worker-1] tests/e2e/checkout.spec.ts:14
should update cart total after adding item
Error: Timeout 5000ms exceeded.
Locator: [data-testid="cart-total"]
Expected string: "$129.00"
Received string: "$45.50"
Call log:
- locator resolved to <span data-testid="cart-total">$45.50</span>
- value never matched expected string within timeout
Network trace: POST /api/cart/recalculate → 200 OK @ 612ms (RẤT NHANH, trước timeout)
Preceding test: should remove item from cart (order_index 7)
Mẹo: Đừng bỏ qua run này chỉ vì error message "giống" Run 1. Chú ý network trace: lần này API trả về nhanh (612ms) chứ không chậm — nghĩa là đây KHÔNG cùng nguyên nhân với Run 1. Giá trị "$45.50" gợi ý cart bị lẫn state từ test trước ("should remove item from cart"), tức nghi vấn order dependency, không phải timing.
Failing Run 3 (Run ID: #1287, 2024-03-18 09:22 UTC)
[worker-4] tests/e2e/checkout.spec.ts:14
should update cart total after adding item
Error: page.click: net::ERR_CONNECTION_RESET at https://ci-preview.internal/api/cart/recalculate
Preceding test: should show product price (order_index 14)
Infra note: CI runner CPU throttled to 92% for 40s window (see infra-metrics dashboard)
Mẹo: Đây là một "red herring" (bằng chứng gây nhiễu) điển hình — lỗi network do hạ tầng CI bị throttle CPU, không liên quan đến logic app. Khi đưa cho AI, hãy đánh dấu rõ các run có kèm chỉ số hạ tầng bất thường (CPU/memory) để nó không gộp nhầm run này vào cùng nhóm nguyên nhân với Run 1 và Run 2.
Failing Run 4 (Run ID: #1301, 2024-03-20 17:45 UTC)
[worker-2] tests/e2e/checkout.spec.ts:14
should update cart total after adding item
Error: Timeout 5000ms exceeded.
Locator: [data-testid="cart-total"]
Expected string: "$129.00"
Received string: "$99.00"
Network trace: POST /api/cart/recalculate → 200 OK @ 5310ms (sau timeout)
Preceding test: should apply discount code (order_index 48)
Mẹo: Run 4 gần như là bản sao của Run 1 (cùng chuỗi giá trị "$99.00", cùng test đứng trước, cùng kiểu timeout). Khi thấy 2 run trùng khớp cao độ như vậy, đó là dấu hiệu mạnh cho một nguyên nhân hệ thống (systematic), không phải ngẫu nhiên — ưu tiên xác nhận giả thuyết này trước.
Passing Run (Run ID: #1290, 2024-03-18 11:05 UTC)
[worker-3] tests/e2e/checkout.spec.ts:14
should update cart total after adding item — PASSED (1399ms)
Network trace: POST /api/cart/recalculate → 200 OK @ 1180ms
Preceding test: should apply discount code (order_index 47)
Mẹo: Run pass này chạy trên cùng worker (worker-3), cùng test đứng trước ("should apply discount code") như Run 1 — chỉ khác là API trả về nhanh hơn (1180ms so với 5187ms). Luôn tìm một run pass "gần giống nhất" với run fail để làm nhóm đối chứng (control group) — sự khác biệt duy nhất giữa hai run gần giống nhau chính là đầu mối root cause đáng tin nhất.
Dùng thứ tự thực thi test (execution order) làm biến chẩn đoán
Từ 4 run fail + 1 run pass trên, ta thấy hai cụm nguyên nhân riêng biệt cùng tồn tại:
- Cụm timing/race condition (Run 1, Run 4): API
/api/cart/recalculateđôi khi vượt quá 5000ms, và assertion timeout ra trước khi UI có cơ hội cập nhật. - Cụm order dependency (Run 2): giá trị "$45.50" không khớp với bất kỳ tính toán hợp lệ nào cho sản phẩm sku-2291 — gợi ý cart bị nhiễm state từ test "should remove item from cart" chạy ngay trước.
Để xác nhận cụm 2, kỹ thuật chẩn đoán hiệu quả nhất là cách ly thứ tự chạy: chạy test một mình (--grep chỉ chạy đúng test này) nhiều lần liên tiếp. Nếu nó pass 100% khi chạy độc lập nhưng fail khi chạy sau "should remove item from cart" trong full suite — order dependency được xác nhận gần như chắc chắn, độc lập với vấn đề timing.
npx playwright test tests/e2e/checkout.spec.ts -g "should update cart total" --repeat-each=20
Mẹo: Đưa cho AI trường order_index và preceding_test của MỌI run (cả pass và fail), không chỉ run fail. Nếu bạn chỉ gửi run fail, AI không có cơ sở để biết "test X đứng trước" có phải là nguyên nhân hay chỉ là trùng hợp — nó cần thấy cả những lần test X đứng trước mà vẫn pass.
Cách dùng AI để validate giả thuyết (hypothesis) về nguyên nhân gốc flaky test?
Có giả thuyết chỉ là bước đầu. Một giả thuyết chưa kiểm chứng ("có thể là race condition") không đáng tin hơn một lời đoán. AI có thể giúp bạn đi từ giả thuyết đến bằng chứng nhân quả (causal), thay vì chỉ tương quan (correlation), bằng cách hỗ trợ thiết kế và diễn giải thực nghiệm.
Ví dụ prompt để chuyển từ giả thuyết sang thực nghiệm:
Dựa trên phân tích trước, ta có 2 giả thuyết cạnh tranh:
H1: Race condition — UI không re-fetch total sau khi /api/cart/recalculate
hoàn tất, chỉ fetch một lần khi mount.
H2: Order dependency — cartId bị cache lại từ test trước, dẫn đến total
tính trên cart sai.
Với mỗi giả thuyết, hãy:
1. Thiết kế 1 thực nghiệm tối thiểu, có thể chạy độc lập, để CHỈ kiểm tra
đúng giả thuyết đó (loại bỏ biến nhiễu từ giả thuyết còn lại).
2. Nêu rõ kết quả nào sẽ XÁC NHẬN và kết quả nào sẽ BÁC BỎ giả thuyết.
3. Ước lượng effort để chạy thực nghiệm (thấp/trung bình/cao).
Mẹo: Luôn yêu cầu AI định nghĩa rõ tiêu chí "bác bỏ" (falsification criteria) trước khi chạy thực nghiệm, theo đúng tinh thần khoa học thực nghiệm. Nếu AI chỉ đưa ra tiêu chí "xác nhận", bạn rất dễ rơi vào việc chạy thực nghiệm cho ra kết quả mơ hồ rồi tự diễn giải theo hướng mình muốn tin.
Vòng lặp validate giả thuyết
Áp dụng vòng lặp 5 bước lặp lại cho đến khi chỉ còn một giả thuyết sống sót:
- Hypothesize — AI/bạn đề xuất giả thuyết cụ thể, có thể kiểm chứng.
- Predict — nếu giả thuyết đúng, kết quả thực nghiệm sẽ như thế nào (định lượng, không mơ hồ).
- Experiment — chạy thực nghiệm tối thiểu, cách ly biến.
- Observe — ghi log/kết quả thô, không diễn giải trước.
- Update belief — đối chiếu observe với predict; giữ, sửa, hoặc loại bỏ giả thuyết.
Yêu cầu AI trình bày kết quả dạng bảng độ tự tin để dễ theo dõi qua nhiều vòng:
| Giả thuyết | Độ tự tin trước thực nghiệm | Thực nghiệm | Kết quả | Độ tự tin sau |
|---|---|---|---|---|
| H1: Race condition | 55% | Inject latency 800ms vào API | Fail 9/10 lần | 90% |
| H2: Order dependency | 40% | Chạy độc lập, không có test trước | Pass 20/20 lần | 15% |
| H3: Network CI flakiness | 20% | Xem lại infra metrics 30 run gần nhất | Chỉ trùng khớp 1/30 run | 5% |
Thiết kế thực nghiệm có kiểm soát cho giả thuyết về timing
Với giả thuyết H1 (race condition do timing), cách kiểm chứng đáng tin nhất là chủ động tạo lại điều kiện lỗi bằng cách inject latency giả vào API, thay vì ngồi chờ CI tự nhiên fail lại:
// tests/e2e/checkout.repro.spec.ts
import { test, expect } from '@playwright/test';
test('reproduce cart-total race condition under artificial latency', async ({ page }) => {
await page.route('**/api/cart/recalculate', async (route) => {
await new Promise((r) => setTimeout(r, 800)); // giả lập network chậm 800ms
await route.continue();
});
await page.goto('/products/sku-2291');
await page.click('[data-testid="add-to-cart"]');
await expect(page.locator('[data-testid="cart-total"]')).toHaveText('$129.00');
});
Chạy lại 10 lần với --repeat-each=10. Nếu test fail ổn định (ví dụ 9/10 hoặc 10/10 lần) khi có latency 800ms nhưng pass 10/10 khi không có latency — bạn đã biến một hiện tượng "ngẫu nhiên" thành một hiện tượng tái lập được (reproducible), điều kiện tiên quyết để coi là đã xác nhận root cause.
Mẹo: Đừng chỉ chạy thực nghiệm 1 lần. Race condition theo bản chất là xác suất — chạy tối thiểu 10 lần và báo tỷ lệ fail (ví dụ "9/10") cho AI, thay vì "pass" hoặc "fail" nhị phân. Tỷ lệ này còn giúp AI (và bạn) ước lượng mức độ nghiêm trọng thực tế trên CI.
Xác nhận nguyên nhân gốc ở cấp độ code
Thực nghiệm ở trên cho bằng chứng hành vi (behavioral evidence), nhưng để "chốt" root cause, cần lần ra đúng dòng code gây ra nó. Đưa cho AI: test đã tái lập lỗi thành công + source code của component liên quan, yêu cầu nó chỉ đích danh cơ chế gây lỗi.
Test Code
// tests/e2e/checkout.repro.spec.ts (đã xác nhận tái lập được, xem phần trên)
Mẹo: Ở bước xác nhận cấp code, hãy đưa lại đúng file test đã dùng để tái lập lỗi (không phải bản gốc chưa sửa). Điều này giúp AI đối chiếu 1-1 giữa hành vi quan sát được và code implementation, tránh nó suy luận trên phiên bản test khác với thực nghiệm đã chạy.
Source Code — Relevant Section
// src/components/CartTotal.tsx
function CartTotal({ cartId }: { cartId: string }) {
const [total, setTotal] = useState<string>('');
useEffect(() => {
// BUG: chỉ fetch một lần khi mount (dependency là cartId, không đổi
// khi "Add to cart" được bấm). Không có cơ chế subscribe/poll để
// biết khi nào /api/cart/recalculate ở nơi khác đã ghi xong DB.
fetchCartTotal(cartId).then(setTotal);
}, [cartId]);
return <span data-testid="cart-total">{total}</span>;
}
Đây chính là root cause được xác nhận: CartTotal fetch tổng tiền đúng một lần khi mount, và không có bất kỳ cơ chế nào để nó biết khi nào request recalculate (được trigger từ nút "Add to cart" ở component khác) đã hoàn tất để fetch lại. Khi recalculate xong nhanh (< 5000ms), component đôi khi vẫn không tự refetch — assertion pass chỉ nhờ timing trùng hợp với một action khác (như route change) vô tình trigger lại useEffect. Khi recalculate chậm hơn bình thường (CI load cao), không có gì trigger lại → assertion timeout thật. Đây giải thích chính xác cả Run 1, Run 4 (chậm → timeout) lẫn việc test "tình cờ" pass phần lớn thời gian trên local (máy nhanh, thường có refetch trùng hợp từ re-render khác).
Mẹo: Khi AI xác nhận root cause ở cấp code, yêu cầu nó viết lại bằng một câu duy nhất theo cấu trúc "X xảy ra vì Y, dẫn đến Z" (ví dụ: "UI không refetch total sau recalculate, dẫn đến hiển thị giá trị cũ khi recalculate chậm hơn ngưỡng timeout"). Một root cause không diễn đạt được thành một câu nhân quả rõ ràng thường là dấu hiệu bạn chưa thực sự tìm ra nó.
Cách quyết định nên fix hay quarantine một flaky test bằng AI?
Không phải mọi flaky test đều nên fix ngay. Có test đáng đầu tư fix ngay (blocking flow quan trọng, root cause đã rõ, fix rẻ); có test nên quarantine (đánh dấu cách ly khỏi luồng block CI, nhưng vẫn track) trong khi chờ ưu tiên hoặc chờ thêm dữ liệu. AI có thể giúp bạn định lượng quyết định này thay vì dựa cảm tính "thấy phiền quá thì skip".
Mẹo: Không bao giờ để một flaky test tồn tại ở trạng thái "không fix, không quarantine, cứ để CI đỏ rồi rerun" — đây là trạng thái tệ nhất, âm thầm bào mòn niềm tin vào tín hiệu CI (đội ngũ dần quen với việc "thấy đỏ cứ rerun", che luôn cả bug thật).
Khung quyết định fix-or-quarantine
Dùng khung chấm điểm dựa trên 4 tiêu chí:
| Tiêu chí | Câu hỏi | Ảnh hưởng tới quyết định |
|---|---|---|
| Độ tự tin root cause | Đã xác nhận bằng thực nghiệm tái lập được chưa? | Cao + rõ → nghiêng về fix ngay |
| Độ phức tạp của fix | Fix có rủi ro phá vỡ luồng khác không? | Thấp → fix ngay; Cao → quarantine + lên plan riêng |
| Mức độ quan trọng nghiệp vụ | Luồng này có phải critical path (checkout, payment) không? | Critical + tần suất flake cao → ưu tiên fix |
| Tần suất & xu hướng flake | Flake rate 30 ngày qua tăng hay giảm? | Đang tăng nhanh → ưu tiên cao dù root cause chưa 100% rõ |
Quy tắc quyết định gợi ý:
- Độ tự tin cao + fix đơn giản → fix ngay trong sprint hiện tại.
- Độ tự tin thấp + luồng critical → quarantine + gắn owner + deadline điều tra (không quarantine vô thời hạn).
- Độ tự tin cao + fix phức tạp/rủi ro → quarantine tạm, lên kế hoạch fix có review kỹ, không rush.
- Độ tự tin thấp + luồng không critical + tần suất thấp → có thể quarantine dài hơn, review lại theo chu kỳ.
Test Profile
{
"test_name": "should update cart total after adding item",
"criticality": "high (checkout critical path)",
"flake_rate_30d": "18%",
"avg_ci_minutes_lost_per_week": 22,
"first_seen_flaky": "2024-02-28",
"owner_team": "checkout-squad"
}
Mẹo: Luôn tính avg_ci_minutes_lost_per_week (thời gian CI lãng phí do rerun/investigate) và đưa vào Test Profile. Con số phút/giờ cụ thể thuyết phục stakeholder ưu tiên fix hiệu quả hơn nhiều so với nói chung "test này hay flaky".
Root Cause Status
{
"status": "confirmed",
"confidence": "90%",
"root_cause_summary": "CartTotal component không refetch sau khi cart recalculate hoàn tất, chỉ fetch một lần khi mount.",
"evidence": ["repro test tests/e2e/checkout.repro.spec.ts (fail 9/10 với latency 800ms)", "so sánh Run #1234 vs Run #1290"]
}
Mẹo: Trường confidence nên là số cụ thể (%), không phải "cao/thấp". Khi confidence dưới khoảng 70%, mặc định nghiêng về quarantine thay vì fix ngay — fix dựa trên root cause chưa chắc thường chỉ che giấu triệu chứng, để lại flaky test "biến tướng" fail theo kiểu khác sau vài tuần.
Team Context
{
"sprint_capacity_this_cycle": "low (2 người đang on-call incident khác)",
"release_deadline": "2024-03-29 (trong 9 ngày)",
"existing_flaky_backlog": 7,
"ci_blocking": true
}
Mẹo: Đưa Team Context cho AI không phải để nó ra quyết định thay bạn, mà để nó đưa ra đề xuất thực tế với nguồn lực hiện có. Một AI không biết team đang thiếu người dễ đề xuất "refactor toàn bộ cơ chế fetch data" — đúng về kỹ thuật nhưng bất khả thi trong 9 ngày trước release.
Tạo ticket quarantine tự động
Khi quyết định quarantine, dùng AI để soạn ticket ngay, tránh việc quarantine "âm thầm" không ai theo dõi:
Soạn một ticket Jira cho việc quarantine test sau, dựa trên Test Profile,
Root Cause Status và Team Context đã cung cấp. Bao gồm:
- Title ngắn, có tag [FLAKY-QUARANTINE]
- Mô tả root cause đã xác nhận (link tới repro test)
- Điều kiện cụ thể để UN-QUARANTINE (không phải "khi nào có thời gian")
- Deadline review (tối đa 2 tuần kể từ ngày quarantine)
- Người/nhóm owner
Kết quả ví dụ AI có thể tạo ra:
[FLAKY-QUARANTINE] checkout.spec.ts — should update cart total after adding item
Root cause (confirmed, 90%): CartTotal không refetch sau khi
/api/cart/recalculate hoàn tất; chỉ fetch một lần khi mount.
Evidence: tests/e2e/checkout.repro.spec.ts, fail 9/10 với latency 800ms.
Điều kiện un-quarantine:
- [ ] CartTotal subscribe/poll lại total sau khi recalculate xong
- [ ] Repro test (checkout.repro.spec.ts) pass 20/20 lần với latency 800ms
- [ ] Test gốc pass ổn định 30 lần liên tiếp trên CI
Deadline review: 2024-04-03
Owner: checkout-squad
Mẹo: Bắt buộc ticket phải có "điều kiện un-quarantine" đo lường được (ví dụ số lần pass liên tiếp cụ thể), không chấp nhận điều kiện mơ hồ như "khi ổn định hơn". Không có tiêu chí rõ, quarantine gần như chắc chắn trở thành vĩnh viễn.
Đo lường chi phí của việc quarantine theo thời gian
Quarantine không phải hành động miễn phí — mỗi test bị quarantine là một khoảng coverage bị tắt tiếng, và rủi ro lớn nhất là nó rơi vào "nghĩa địa quarantine" (quarantine graveyard): bị cách ly rồi không ai quay lại. Theo dõi các chỉ số sau theo thời gian, lý tưởng trên một dashboard:
- Tổng số test đang bị quarantine, và xu hướng tăng/giảm theo tuần.
- Số ngày trung bình một test ở trạng thái quarantine (tuổi trung bình).
- Số test quá hạn deadline review đã đặt trong ticket.
- Coverage bị mất tương đối (bao nhiêu % luồng nghiệp vụ đang không được test tự động bảo vệ vì bị quarantine).
Dùng AI để tạo báo cáo định kỳ (ví dụ mỗi thứ Hai) cho buổi triage flaky test hàng tuần:
Dựa trên danh sách test đang quarantine (kèm ngày quarantine, deadline
review, root cause status), soạn báo cáo ngắn cho buổi triage:
1. Test nào đã quá deadline review — cần escalate ngay.
2. Test nào có root cause "unknown" quá 14 ngày — rủi ro bị bỏ quên.
3. Tổng coverage-critical-path đang bị quarantine — có luồng nào
thuộc payment/checkout không được test tự động bảo vệ không?
Mẹo: Đặt một ngưỡng cứng (ví dụ: không quarantine quá 30 ngày mà không có review) và để CI tự động fail một job riêng (không phải job chính) khi có test vượt ngưỡng — biến việc "theo dõi chi phí quarantine" từ một tác vụ thủ công dễ quên thành một cảnh báo tự động không thể bỏ qua.