Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 6 — Đánh giá chất lượng test: coverage, mutation testing và test giả

Đã đọc 0/10 mục · còn khoảng 4 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Đá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 assert kiểm kết quả/tương tác.
  • Assertion cụ thể: assert x == 80 tốt hơn assert 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ùng assert 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 result với result là số dương luôn true — kiểm như không kiểm.
Bài 7 ta đưa agent vào pipeline CI/CD thật để nó làm việc liên tục, không chỉ chạy tay.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi