Bỏ qua để đến nội dung

Giải thích mutation testing

Spec: ISO/IEC/IEEE 29119-4 Spec: PHPUnit Evidence: Test-backed

Line coverage cho bạn biết một dòng đã chạy trong bộ kiểm thử. Nó không cho bạn biết liệu có bài kiểm thử nào sẽ thất bại nếu dòng đó sai hay không. Mutation testing lấp khoảng trống đó bằng cách cố ý làm hỏng mã và kiểm tra xem các bài kiểm thử có nhận ra hay không. Trang này giải thích mutation score có nghĩa là gì và cách NextPDF dùng nó như một công cụ chẩn đoán, chứ không phải một thành tích để trưng bày.

Coverage là một trong những thước đo được tin tưởng nhất trong kiểm thử, nhưng cũng là một trong những thước đo dễ gây hiểu lầm nhất. Một bài kiểm thử gọi một method mà không assert gì vẫn có thể đi qua mọi dòng trong đó: coverage hoàn hảo, khả năng phát hiện lỗi bằng không. Tài liệu tiêu chuẩn nêu rõ rằng thứ tự giữa các tiêu chí coverage không nói lên khả năng phát hiện lỗi của chúng. Khả năng đó là thuộc tính mà tài liệu gọi là test effectiveness (ISO/IEC/IEEE 29119-4, §C.2.4). Tỷ lệ coverage và lời bảo đảm tìm được lỗi là hai tuyên bố khác nhau.

Với một engine PDF, đây không phải chuyện lý thuyết suông. Một bước kiểm tra byte-range của chữ ký, một offset cross-reference, một nhánh mã hóa — các bài kiểm thử có thể “phủ” trọn vẹn tất cả những thứ này mà chưa từng assert giá trị thực sự quan trọng. Một bộ kiểm thử xanh dựa trên những bài kiểm thử yếu còn tệ hơn một khoảng trống được thừa nhận trung thực, vì nó khiến mọi người chủ động bỏ qua việc xem lại.

  • Mutation testing tạo ra hàng nghìn chỉnh sửa nhỏ, có chủ ý (mutant) trên mã nguồn — đổi một < thành <=, một + thành -, một giá trị return — rồi chạy lại các bài kiểm thử với từng thay đổi đó.
  • Nếu một bài kiểm thử thất bại với một mutant, mutant đó bị diệt: nghĩa là có bài kiểm thử nào đó thật sự đã assert hành vi ấy. Nếu mọi bài kiểm thử vẫn vượt qua, mutant đã thoát: hành vi đó được chạy nhưng chưa bao giờ được kiểm tra.
  • Chỉ số Mutation Score Indicator (MSI) về cơ bản là số mutant bị diệt chia cho tổng số mutant không tương đương. Nó đo lường liệu các bài kiểm thử của bạn có phát hiện được thay đổi hay không, chứ không phải liệu chúng có chạy mã hay không.
  • Một số mutant là tương đương — chúng không thể thay đổi hành vi quan sát được, nên không bài kiểm thử nào diệt được chúng. Tính những mutant này là thất bại sẽ không trung thực. NextPDF chứng minh và ghi vào sổ những mutant này thay vì loại bỏ chúng một cách tùy tiện.
  • NextPDF dùng MSI để tìm và củng cố những bài kiểm thử yếu. Đó là một cổng chẩn đoán trong continuous integration, không phải một con số để tiếp thị.

Mutation testing được chạy trên engine bằng mutator Infection. Nó được cấu hình trên cây mã nguồn production, với các họ mutator arithmetic, boolean, conditional-boundary, equality, return-value và removal được bật — đây chính là những toán tử làm lộ logic “đã chạy nhưng chưa được assert”. Quy trình này mang tính cơ học:

  1. Start green The suite must pass before mutation begins.
  2. Mutate Apply one small, deliberate change to the source.
  3. Re-run Run the tests that cover the mutated line.
  4. Killed A test failed — the behaviour is genuinely asserted.
  5. Escaped All tests still pass — a weak spot to strengthen.
  6. Equivalent No test can kill it because behaviour is unchanged — proven and ledgered, not scored as a miss.
Vòng lặp mutation testing mà NextPDF chạy: bắt đầu từ các bài kiểm thử xanh, tạo một mutant, chạy lại những bài kiểm thử bao phủ dòng đó, rồi phân loại mutant là đã diệt (một bài kiểm thử bắt được nó), đã thoát (một khoảng trống có-coverage-nhưng-không-có-assertion cần khắc phục), hoặc được-chứng-minh-tương-đương (không bài kiểm thử nào diệt được; được ghi vào sổ, không bị tính vào điểm).

Hai lựa chọn thiết kế khiến con số này đáng tin cậy. Thứ nhất, điểm số được gắn như một cổng. Continuous integration ép một ngưỡng MSI tối thiểu (và một covered-MSI tối thiểu) và chạy một biến thể giới hạn theo diff trên những dòng đã thay đổi. Nhờ vậy, một thay đổi có thêm mã nhưng không thêm assertion thực sự sẽ bị bắt ngay khi review, chứ không phải bị phát hiện về sau. Thứ hai, NextPDF không âm thầm bỏ qua những mutant bất tiện. Những mutant thực sự tương đương về ngữ nghĩa — chẳng hạn !== so với != khi strict typing bảo đảm cả hai toán hạng cùng kiểu — được ghi vào một mutation ledger kèm một bài kiểm thử chứng minh rõ ràng tính tương đương. Nhờ vậy, số mutant thoát phản ánh những khoảng trống thực sự, chứ không phải nhiễu do cách tính điểm. PHPStan Level 10 cộng với strict_types cộng với typed properties là những gì làm cho các chứng minh tương đương đó vững chắc.

Evidence: Test-backed Mutation testing được cấu hình trong engine, trên các thư mục mã nguồn production, với các họ mutator có khả năng làm lộ hành vi được bật. Nó được thực thi như một cổng continuous-integration với một MSI tối thiểu và một biến thể giới hạn theo diff. Đó là một bước kiểm tra trong build, không phải một thủ tục hình thức.

Evidence: Test-backed Vấn đề mutant tương đương được xử lý một cách trung thực. Các mutant tương đương về ngữ nghĩa được phân loại và có các bài kiểm thử chuyên biệt chứng minh tính tương đương trong một mutation ledger, với độ vững chắc của mỗi chứng minh dựa trên PHPStan Level 10 cộng với strict typing. Do đó, số mutant thoát đại diện cho hành vi thực sự không bị phát hiện, không phải nhiễu không thể diệt được bị tính phồng lên thành một điểm số trông tệ hơn.

Evidence: Standard-backed Mutation là một kỹ thuật được công nhận, không phải phát minh của NextPDF. Spec: ISO/IEC/IEEE 29119-4, §B.2.4 mô tả việc áp dụng các đột biến tổng quát lên các phần tử của một đặc tả để suy ra các đột biến cụ thể phục vụ kiểm thử. Sở dĩ cần đến kỹ thuật này là vì chính tiêu chuẩn đó nêu rằng thứ tự subsumes của các tiêu chí coverage không sắp xếp chúng theo khả năng bộc lộ lỗi (ISO/IEC/IEEE 29119-4, §C.2.4).

Evidence: Standard-backed Bản thân coverage được định nghĩa rõ ràng và có giới hạn. Spec: PHPUnit phân biệt line, branch và path coverage. Line coverage chỉ ghi nhận rằng một dòng thực thi đã chạy. Việc nắm được định nghĩa này chính là điều làm cho sự thiếu sót của nó trở nên rõ ràng.

Điểm mấu chốt không phải là câu lệnh cụ thể — mà là điều một mutant đã thoát cho bạn biết:

<?php
declare(strict_types=1);
final class ByteRange
{
// Suppose the production guard is:
// if ($offset < 0) { throw new InvalidByteRange(); }
public function assertNonNegative(int $offset): void
{
if ($offset < 0) {
throw new InvalidByteRange('offset must be >= 0');
}
}
}
// A test that EXECUTES this line but does not assert the boundary:
// $byteRange->assertNonNegative(5); // no exception expected, none asserted
// gives 100% line coverage of assertNonNegative().
//
// Mutation flips `< 0` to `<= 0`. Behaviour now differs ONLY at $offset === 0.
// If no test passes 0 and asserts what happens, every test still passes:
// the mutant ESCAPED. Coverage said "tested"; mutation said "the boundary
// is unasserted". The fix is a test that pins offset === 0, not a higher
// target.
//
// composer mutation:diff → mutate only changed lines, enforce min MSI
// composer mutation:full → full-tree mutation gate

Mutant đã thoát đó chính là toàn bộ giá trị của kỹ thuật này. Nó định vị một assertion cụ thể thật sự còn thiếu, trong khi báo cáo coverage lại cho rằng phần đó đã được kiểm thử đầy đủ.

Hiểu lầm nổi bật nhất là coi mutation score như một điểm số cần tối đa hóa. Một MSI rất cao đạt được nhờ viết các bài kiểm thử để diệt mutant cũng rỗng tuếch như coverage cao đạt được nhờ gọi method mà không assert. Thước đo đã bị lách và không còn đo được khả năng phát hiện. NextPDF dùng MSI để tìm những bài kiểm thử yếu. Kết quả cần đạt là một assertion tốt hơn; việc khoe điểm rõ ràng không phải mục đích.

Hiểu lầm thứ hai là cho rằng mọi mutant còn sống đều là khiếm khuyết trong các bài kiểm thử. Một số mutant thực sự tương đương và không thể bị diệt bởi bất kỳ bài kiểm thử nào, vì chúng không làm thay đổi hành vi quan sát được. Tính những mutant đó là thất bại sẽ tạo ra một điểm số không trung thực, thấp một cách giả tạo và tập cho mọi người bỏ qua báo cáo. Cách NextPDF giải quyết là chứng minh tính tương đương một cách rõ ràng và ghi vào sổ, chứ không âm thầm loại nó đi hay giả vờ rằng con số tệ hơn thực tế.

Mutation testing đo lường liệu các bài kiểm thử có phát hiện được những thay đổi được tiêm vào hay không. Nó không chứng minh mã là đúng. Nó không đo hiệu năng hay tính tuân thủ. Nó không thể diệt một mutant thực sự tương đương. Mutation score hiện tại, ngưỡng MSI tối thiểu đang áp dụng, số lượng mutant tương đương đã ghi vào sổ, và mọi con số coverage đều là những tín hiệu chất lượng luôn thay đổi, được tạo ra từ các artifact continuous-integration và công bố cùng với bản build. Chúng được cố tình không đưa vào đây, vì một con số dán vào văn bản sẽ trở nên lỗi thời và biến thành một lời nói dối nhỏ. Sự thật ổn định duy nhất mà trang này nêu là PHPStan Level 10, và đó là một thuộc tính cấu hình làm nền tảng cho các chứng minh tương đương, chứ không phải một phép đo.

Việc lựa chọn mutator, các ngưỡng và chính sách ledger thuộc về cấu hình mutation của engine và có thể thay đổi theo thời gian. Cấu hình đó là nguồn quyết định nếu có lúc nó mâu thuẫn với trang này. Không có tuyên bố nào ở đây về test effectiveness của bất kỳ thư viện nào khác.

  • Tháp kiểm thử của NextPDF — năm tầng mà mutation testing kiểm định khả năng phát hiện lỗi thực sự của các bài kiểm thử ở từng tầng.
  • Strict types, ở mọi nơi — cách PHPStan Level 10 và strict typing làm cho các chứng minh mutant tương đương trở nên vững chắc.
  • Golden-file testing — một tầng khác được mutation testing giúp xác nhận khả năng phát hiện.
  • Mutant — một thay đổi nhỏ, đơn lẻ và có chủ ý trong mã nguồn, dùng để kiểm tra xem bộ kiểm thử có nhận ra thay đổi đó hay không.
  • Mutant bị diệt — một mutant khiến ít nhất một bài kiểm thử thất bại; hành vi đó thực sự đã được assert.
  • Mutant đã thoát — một mutant khiến mọi bài kiểm thử vẫn vượt qua. Hành vi đã được chạy nhưng không bao giờ được assert — một điểm yếu cần khắc phục.
  • Mutant tương đương — một mutant không thể thay đổi hành vi quan sát được, nên không bài kiểm thử nào diệt được nó. NextPDF chứng minh và ghi vào sổ những mutant này.
  • MSI (Mutation Score Indicator) — đại khái là số mutant bị diệt chia cho tổng số mutant không tương đương; một thước đo khả năng phát hiện, không phải khả năng chạy.
  • Line coverage — một thước đo chỉ ghi nhận rằng một dòng thực thi đã chạy trong bộ kiểm thử; được định nghĩa bởi PHPUnit, và tự nó là không đủ.