Đánh giá chất lượng test: coverage, mutation testing và test giả
Agent có thể sinh 200 test đạt 95% coverage mà vẫn vô dụng. Coverage cao chỉ nói code được chạy, không nói hành vi được kiểm. Bài này dạy cách đo chất lượng thật để agent tối ưu đúng thứ.
Vì sao coverage nói dối
def test_apply_discount():
apply_discount(100, "VIP") # chạy hàm, KHÔNG assert gì
Test này đạt coverage nhưng không kiểm gì — xóa cả hàm nó vẫn "pass". Đây là assertion-free test, một dạng test giả agent rất hay sinh ra để "làm đẹp" số coverage. Bạn cần bộ lọc phát hiện.
Mutation testing: thước đo thật
Mutation testing cố tình "đục" bug vào code (đổi >= thành >, + thành -, xóa dòng) rồi chạy test. Nếu test fail khi có bug -> test tốt (đã "giết" mutant). Nếu test vẫn pass dù code đã hỏng -> test yếu (mutant sống sót).
# Python: mutmut | JS: stryker
pip install mutmut
mutmut run --paths-to-mutate=src/discount.py
mutmut results # xem mutant nào sống sót
Mutation score = mutant bị giết / tổng mutant. Đây là tín hiệu chất lượng mạnh hơn coverage rất nhiều, và là mục tiêu tối ưu tốt hơn cho agent.
Bộ tiêu chí chất lượng test agent nên tự kiểm
- Có assertion thật: mỗi test ít nhất một
assertkiểm kết quả/tương tác. - Assertion cụ thể:
assert x == 80tốt hơnassert x is not None. - Một hành vi mỗi test: fail thì biết ngay hỏng gì.
- Không phụ thuộc thứ tự: chạy riêng lẻ vẫn pass (independent).
- Tất định (deterministic): không random, không thời gian thật -> không flaky.
- Giết được mutant: qua mutation testing.
Các bước để agent tự đánh giá chất lượng
- Sau khi test pass, chạy lint test-smell: quét test không có
assert, test dùngassert True, test trùng lặp. - Chạy mutation testing trên module vừa test; đọc danh sách mutant sống sót.
- Với mỗi mutant sống, đưa cho agent: "bug này không bị test bắt" -> yêu cầu sinh test giết mutant đó.
- Kiểm tính tất định: chạy test 3 lần, nếu kết quả đổi -> đánh dấu flaky, yêu cầu sửa (mock random/time).
- Đặt ngưỡng kép: coverage ≥ X và mutation score ≥ Y mới coi là đạt.
def is_good_enough(result):
return (result.coverage >= 0.85
and result.mutation_score >= 0.70
and result.assertion_free_tests == 0
and not result.flaky)
Khung tư duy: "Coverage là điều kiện cần, mutation là điều kiện đủ"
Coverage đảm bảo bạn chạm code; mutation đảm bảo bạn kiểm code. Dùng coverage làm cổng lọc rẻ tiền (chạy nhanh), mutation làm cổng chất lượng đắt tiền (chạy chậm, chỉ trên module quan trọng). Đừng chạy mutation cho toàn repo mỗi lần — chậm và tốn kém; chạy có chọn lọc.
Ví dụ mutant sống sót dạy ta điều gì
Mutmut đổi age >= 18 thành age > 18. Nếu test của bạn không có case age == 18, mutant sống -> lộ ra bạn thiếu test ranh giới. Mutation testing tự chỉ ra lỗ hổng off-by-one mà coverage không thấy. Đây chính là cầu nối với edge case ở Bài 5.
Biến mutant sống sót thành tín hiệu refine
Điều tuyệt vời của mutation testing với agent là mỗi mutant sống sót là một nhiệm vụ cụ thể, có thể hành động ngay. Thay vì nói mơ hồ "test chưa tốt", bạn đưa cho agent: "đột biến này (đổi >= thành >) không bị test nào bắt — hãy viết test giết nó". Đây là tín hiệu refine mạnh hơn nhiều so với chỉ số coverage, vì nó chỉ đúng chỗ hành vi chưa được kiểm. Vòng lặp trở thành: chạy mutation → lấy mutant sống → sinh test nhắm đúng → chạy lại → lặp cho đến khi mutation score đạt ngưỡng. Cách này nâng chất lượng suite lên tầm mà chỉ tối ưu coverage không bao giờ đạt tới.
Cân bằng chi phí mutation testing
Mutation testing chạy lại toàn bộ test cho mỗi đột biến nên rất tốn thời gian — một module có thể sinh hàng trăm mutant. Đừng chạy nó cho toàn repo mỗi lần. Chiến lược thực dụng: dùng coverage làm cổng lọc rẻ và nhanh cho mọi thay đổi, còn mutation chỉ chạy trên các module quan trọng (thanh toán, xác thực, tính toán tiền) hoặc theo lịch định kỳ (nightly). Với agent, hãy giới hạn mutation vào đúng những dòng vừa thay đổi trong PR để giữ thời gian và chi phí trong tầm kiểm soát.
Sai lầm thường gặp
- Coi coverage là mục tiêu cuối: agent tối ưu số coverage bằng test rỗng assertion.
- Không chạy mutation testing: không bao giờ biết test có thật sự bắt bug hay không.
- Chạy mutation cho toàn repo mỗi lần: cực chậm, đốt CI, đội chi phí.
- Bỏ qua flaky test: test lúc pass lúc fail làm hỏng niềm tin vào cả suite.
- Assertion mơ hồ:
assert resultvới result là số dương luôn true — kiểm như không kiểm.