Ba phần trước đều có một số liệu lơ lửng: độ phủ. Phần 1 có integration phủ 59% mà lọt lỗi làm tròn; phần 2 có bản trước phủ 84,8% mà mù cả nhánh giờ vàng. Độ phủ nói dòng nào được chạy; nó không nói dòng nào được khẳng định. Mutation testing đo thẳng vào khoảng cách đó: sửa mã sản phẩm một chút — đổi < thành <=, đổi * thành / — rồi chạy lại bộ test. Test đỏ là đột biến bị giết; test vẫn xanh là bộ test không nhìn thấy dòng đó dù có chạy qua. Bài này chạy gremlins trên bốn gói: hai bộ test cố ý khác nhau cho cùng một hàm, và hai gói thật từ phần 1 và phần 2.
Cùng một hàm, hai bộ test cùng phủ 100%

Hình 1: Hàm tính phí với sàn, trần và mốc miễn phí; bộ weak chạy qua mọi dòng nhưng chỉ kiểm "không âm", bộ strong khẳng định giá trị tại từng biên.
Fee(amount, tier) tính phí giao dịch: 2% mặc định, silver 1,5%, gold 1% và miễn phí nếu gold dưới 100.000; kẹp sàn 1.000 và trần 50.000; từ chối số tiền không dương. Hai bộ test:
- weak gọi hàm với tám cặp đầu vào chạm đủ mọi nhánh — âm, 0, nhỏ, lớn, ba hạng, vượt trần — nhưng chỉ khẳng định hai điều yếu: không lỗi thì phí không âm, số tiền không dương thì phải lỗi. Đây là kiểu test người ta viết để "lên độ phủ", và nó lên thật: 100%.
- strong dùng đúng những đầu vào đó cộng vài ca biên (đúng bằng sàn, đúng bằng trần, đúng mốc 100.000 của gold) và khẳng định giá trị chính xác cùng lỗi mong đợi. Cũng 100% độ phủ.
Hai gói thật để đối chiếu: after/core.go của phần 2 với bảng 24 ca, và shop của phần 1 với bốn unit test cho price.go và không có unit test cho repo và handler.
Công cụ là gremlins: nó sinh đột biến kiểu đổi biên điều kiện (< ↔ <=), phủ định điều kiện (< ↔ >=), đổi phép toán (* ↔ /, + ↔ -), rồi với mỗi đột biến chạy lại bộ test và ghi KILLED, LIVED, hoặc NOT COVERED nếu không test nào chạy qua dòng đó. Một bẫy công cụ đáng ghi: timeout mỗi đột biến mặc định bằng 3 lần thời gian chạy test gốc — test gốc chạy 1 ms nên timeout là 3 ms và mọi đột biến đều TIMED OUT ở lần chạy đầu; phải nâng hệ số lên mới có kết quả.
Đo thật: điểm đột biến so với độ phủ

Hình 2: Bảng điểm bốn gói, tám đột biến sống trong weak, và ba đột biến sống còn lại được đọc từng cái một.
| gói | độ phủ | đột biến | killed | lived | not covered | hiệu lực |
|---|---|---|---|---|---|---|
| weak | 100% | 10 | 2 | 8 | 0 | 20% |
| strong | 100% | 10 | 8 | 2 (tương đương) | 0 | 80% |
| after/core (phần 2) | 100% lõi | 27 | 20 | 0 | 7 (vỏ) | 100% |
| shop (phần 1) | 33,8% | 32 | 20 | 1 | 11 | 95,2% |
Cùng 100% độ phủ, chênh nhau 60 điểm. weak giết 2/10, strong giết 8/10. Tám đột biến sống sót trong weak gồm cả đổi phép nhân thành phép chia trong công thức phí — amount * pct / 10000 thành amount / pct / 10000 — và test vẫn xanh, vì "phí không âm" đúng với mọi công thức trả về số dương. Đổi mốc miễn phí gold từ < 100000 thành >= 100000: xanh. Bỏ hẳn sàn và trần: xanh. Hai đột biến duy nhất bị giết là ở amount <= 0, vì weak có đúng một khẳng định có nghĩa: số tiền không dương phải lỗi. Độ phủ 100% của weak là thật — mọi dòng đều chạy — và vô nghĩa.
strong 80%, và hai con sống sót không giết được. Cả hai ở dòng kẹp: if fee < 1000 { fee = 1000 } đổi thành <= — khi fee == 1000, nhánh mới chạy và gán 1.000 cho một biến đang bằng 1.000. Trần > 50000 cũng vậy. Đây là đột biến tương đương: mã sau đột biến có hành vi y hệt mã gốc với mọi đầu vào, nên không test nào phân biệt được. Hiệu lực 80% của strong thực chất là 100% có nghĩa. Công cụ không tự biết điều đó; người đọc phải nhìn từng đột biến sống và quyết định: thiếu test, hay tương đương.
Bảng 24 ca của phần 2 giết 20/20. Lõi Quote với bốn ca biên giờ vàng, hai ca làm tròn, các ca hợp lệ và không hợp lệ — mọi đột biến gremlins sinh ra trong core.go đều có ít nhất một dòng của bảng đỏ. Bảy đột biến NOT COVERED nằm hết ở shell.go, đúng như thiết kế: vỏ không có unit test, chỉ có hai ca tích hợp (gremlins không chạy tag integration). Độ phủ toàn gói 48,8% trông tệ; điểm đột biến nói đúng hơn: phần có test thì test đủ chặt, phần không có test thì rõ ràng là không có.
shop để sống một đột biến thật. it.UnitCents < 0 thành <= 0 ở hàm Validate: không ca nào trong bốn unit test của phần 1 có đơn giá bằng 0 hợp lệ, nên nếu ai đó sửa nhầm thành từ chối giá 0, bộ test không biết. Bảng 24 ca của phần 2 có ca "giá 0 hợp lệ" — không phải ngẫu nhiên, mà vì khi viết bảng tôi đã nghĩ tới biên này. Mutation testing là cách để máy chỉ ra biên bạn chưa nghĩ tới, thay vì chờ phần 2 của mỗi dự án.
Đánh đổi cần cân nhắc
Điểm đột biến đắt. Mỗi đột biến là một lần biên dịch và chạy lại bộ test; 32 đột biến của shop là 32 lần go test. Với gói có test tích hợp chậm thì không chạy được mỗi commit; cách thường làm là chạy theo lịch, hoặc chỉ trên file vừa đổi, hoặc chỉ trên gói lõi thuần — nơi điểm đột biến có ý nghĩa nhất và rẻ nhất.
100% không phải mục tiêu, và điểm thấp không phải tội. Đột biến tương đương có thật và không giết được; một gói 80% với hai đột biến tương đương là đủ. Ngược lại, điểm cao ở một gói mà phần lớn đột biến NOT COVERED (như shop 95,2% nhưng 11 dòng không có test) không nói gì về phần không được kiểm. Đọc ba cột, không đọc một số.
Đột biến chỉ ra chỗ thiếu khẳng định, không chỉ ra khẳng định nào. amount * pct / 10000 sống sót nói "không test nào kiểm công thức" — người viết vẫn phải quyết định giá trị đúng là gì. Vì thế nó ghép tốt với phần 5: đột biến chỉ chỗ, tính chất hoặc bảng ca lấp chỗ.
Bẫy công cụ. Timeout theo hệ số của thời gian test gốc là hợp lý với test chạy giây, vô nghĩa với test chạy mili-giây; lần đầu chạy ra toàn TIMED OUT dễ bị hiểu nhầm là mã treo. Và gremlins chạy theo build tag mặc định, nên test tích hợp đứng ngoài — đúng ý ở đây, nhưng phải biết.
Ba ý mang về
- Độ phủ đo dòng được chạy, điểm đột biến đo dòng được khẳng định: cùng 100% dòng, weak 20% và strong 80%; công thức phí đổi nhân thành chia mà weak vẫn xanh.
- Đọc từng đột biến sống, đừng đọc một con số: hai con sống của strong là tương đương (kẹp sàn trần với
<=), không giết được và không cần giết; con sống của shop là biên giá 0 thật sự thiếu test. - Bộ test có khẳng định đúng chỗ thì tự nhiên giết hết: bảng 24 ca của phần 2 giết 20/20 mà không viết để chống đột biến. Chạy mutation testing theo lịch trên gói lõi thuần, dùng kết quả để thêm ca vào bảng.
Nguồn
- Richard DeMillo, Richard Lipton, Frederick Sayward — Hints on Test Data Selection: Help for the Practicing Programmer (IEEE Computer, 1978), nguồn gốc mutation testing
- go-gremlins — tài liệu và danh sách đột biến: https://gremlins.dev/
- Google — State of Mutation Testing at Google (ICSE-SEIP 2018): https://research.google/pubs/state-of-mutation-testing-at-google/
- Yue Jia, Mark Harman — An Analysis and Survey of the Development of Mutation Testing (IEEE TSE 2011)
- Go — gói
covervà giới hạn của độ phủ câu lệnh: https://go.dev/blog/cover
Phần sau bước ra khỏi một gói: hai dịch vụ nói chuyện qua HTTP, bên nào đổi JSON là bên kia vỡ — contract testing đo xem hợp đồng consumer–provider bắt thay đổi phá vỡ sớm hơn test tích hợp bao nhiêu, với giá nào.