Kiểm thử đột biến (Mutation Testing) với Infection PHP
Bạn có một dự án với Code Coverage 100%. Sếp vui, team vui, CI xanh lè. Nhưng liệu bộ test đó có thực sự bắt được lỗi không? Hay nó chỉ đang chạy qua code mà không thực sự assert điều gì quan trọng?
Đây là lúc Mutation Testing (Kiểm thử đột biến) xuất hiện. Trong hệ sinh thái PHP, công cụ nổi tiếng nhất là Infection.
Mutation Testing là gì?
Nguyên lý rất đơn giản:
- Công cụ sẽ tạo ra các phiên bản "đột biến" (mutants) của code production của bạn.
- Ví dụ: đổi
+thành-, đổitruethànhfalse, đổi>thành>=, xóa một dòng code... - Sau đó, nó chạy bộ test của bạn với code bị đột biến đó.
- Nếu bộ test vẫn... Green (Pass) -> Nghĩa là Mutant Escaped (Lỗi đã lọt lưới). Test của bạn quá yếu, code sai mà test vẫn đúng.
- Nếu bộ test Red (Fail) -> Nghĩa là Mutant Killed. Test của bạn tốt, nó phát hiện ra sự thay đổi bất thường.
Mục tiêu của chúng ta là "Giết" càng nhiều Mutant càng tốt (MSI - Mutation Score Indicator cao).
Cài đặt Infection trong Laravel
Infection hoạt động tốt nhất khi bạn đã có nền tảng PHPUnit hoặc Pest.
composer require --dev infection/infection
Khởi tạo cấu hình:
./vendor/bin/infection --init
Nó sẽ tạo file infection.json.dist.
Ví dụ thực tế
Giả sử bạn có code tính giảm giá:
public function isEligibleForDiscount(User $user): bool
{
return $user->age >= 18 && $user->hasSubscription();
}
Và bạn viết test (kém chất lượng) như sau:
// Test case duy nhất
it('checks discount eligibility', function () {
$user = User::factory()->create(['age' => 20, 'subscribed' => true]);
expect($this->service->isEligibleForDiscount($user))->toBeTrue();
});
Test này pass và cover 100% dòng code.
Khi Infection chạy:
-
Nó tạo một mutant đổi
>=thành>:return $user->age > 18 ...-> Với user 20 tuổi, mutant này vẫn trả vềtrue. Test vẫn Pass. -> Mutant Escaped. Lý do: Chúng ta thiếu test case biên (boundary) tại giá trị 18. -
Nó tạo mutant đổi
&&thành||:return ... || ...-> Test vẫn Pass. -> Mutant Escaped.
Khắc phục
Infection sẽ chỉ cho bạn chính xác dòng nào bị escape và mutant là gì. Để "kill" các mutant trên, bạn phải viết thêm test case:
it('allows exactly 18', function() { ... }); // Kill mutant > thành >=
it('denies under 18', function() { ... });
it('requires subscription', function() { ... }); // Kill mutant && thành ||
Chạy Infection
Vì Mutation Testing chạy test suite rất nhiều lần (mỗi mutant một lần), nó rất chậm.
- Chỉ nên chạy cho các file thay đổi (git diff).
- Hoặc chạy định kỳ (nightly build) thay vì mỗi commit.
Lệnh chạy chỉ cho các file đã sửa đổi:
./vendor/bin/infection --git-diff-filter=AM
Hoặc chạy song song để nhanh hơn:
./vendor/bin/infection --threads=4
Kết luận
Code Coverage chỉ cho biết dòng code đã được thực thi hay chưa. Mutation Score cho biết dòng code đã được kiểm chứng kỹ hay chưa.
Nếu bạn muốn xây dựng hệ thống Critical (Payment, Banking, Healthcare) bằng Laravel, Infection PHP là công cụ bắt buộc phải có trong CI pipeline để đảm bảo sự tự tin tuyệt đối vào chất lượng code.