ข้ามไปยังเนื้อหา

ทำความเข้าใจการทดสอบแบบกลายพันธุ์

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

Line coverage บอกเพียงว่าบรรทัดหนึ่ง ถูกรัน ระหว่างการรันชุดทดสอบ แต่ไม่ได้บอกว่าการทดสอบใดจะ ล้มเหลว หากบรรทัดนั้นผิด การทดสอบแบบกลายพันธุ์อุดช่องว่างนี้ด้วยการจงใจทำให้โค้ดผิด แล้วตรวจสอบว่าการทดสอบสังเกตเห็นหรือไม่ หน้านี้อธิบายว่าคะแนนการกลายพันธุ์หมายถึงอะไร และ NextPDF ใช้คะแนนนั้นเป็นเครื่องมือวินิจฉัยอย่างไร ไม่ใช่เป็นถ้วยรางวัล

Coverage เป็นเมตริกการทดสอบที่ได้รับความเชื่อถือมากที่สุดอย่างหนึ่ง และเป็นเมตริกที่ทำให้เข้าใจผิดได้มากที่สุดอย่างหนึ่งเช่นกัน การทดสอบที่เรียกเมธอดหนึ่งแต่ไม่ assert สิ่งใดเลยจะรันทุกบรรทัดในเมธอดนั้น จึงมี coverage สมบูรณ์แบบแต่มีความสามารถในการตรวจจับเป็นศูนย์ เอกสารมาตรฐานระบุชัดเจนว่าการจัดลำดับระหว่างเกณฑ์ coverage ไม่ได้บ่งชี้ถึงความสามารถในการเปิดเผยข้อบกพร่องแต่อย่างใด ความสามารถดังกล่าวคือคุณสมบัติที่มาตรฐานเรียกว่า test effectiveness (ISO/IEC/IEEE 29119-4, §C.2.4) เปอร์เซ็นต์ coverage และคำรับประกันว่าจะพบข้อบกพร่องเป็นคนละคำกล่าวอ้างกัน

สำหรับเอนจิน PDF เรื่องนี้ไม่ใช่แค่ทฤษฎีทางวิชาการ การตรวจสอบ byte-range ของลายเซ็น offset ของ cross-reference และแขนงตรรกะการเข้ารหัสต่างก็เป็นจุดที่การทดสอบสามารถ “cover” ได้ครบถ้วนโดยไม่เคย assert ค่าที่สำคัญเลย ชุดทดสอบที่ขึ้นสีเขียวทั้งที่การทดสอบอ่อนแอนั้นแย่กว่าการมีช่องว่างที่ยอมรับกันตรง ๆ เพราะทำให้ไม่มีใครเข้าไปตรวจสอบอย่างจริงจัง

  • การทดสอบแบบกลายพันธุ์ สร้างการแก้ไขโค้ดต้นทางเล็ก ๆ โดยจงใจนับพันรายการ (mutant) — เช่นเปลี่ยน < เป็น <= เปลี่ยน + เป็น - หรือเปลี่ยนค่าที่ return — แล้วรันการทดสอบใหม่กับการแก้ไขแต่ละรายการ
  • หากการทดสอบอย่างน้อยหนึ่งตัวล้มเหลวกับ mutant ใด mutant นั้นถือว่าถูก ฆ่า (killed) กล่าวคือมีการทดสอบบางตัว assert พฤติกรรมนั้นไว้จริง หากการทดสอบทุกตัวยังคงผ่าน mutant นั้นถือว่า รอด (escaped) กล่าวคือพฤติกรรมถูกรันแล้วแต่ไม่เคยถูกตรวจสอบ
  • ค่า Mutation Score Indicator (MSI) โดยคร่าว ๆ คือจำนวน mutant ที่ถูกฆ่าหารด้วยจำนวน mutant ที่ไม่สมมูลทั้งหมด ค่านี้วัดว่าการทดสอบ ตรวจจับ การเปลี่ยนแปลงได้หรือไม่ ไม่ใช่ว่าการทดสอบ รัน โค้ดหรือไม่
  • mutant บางตัวเป็น สมมูล (equivalent) — ไม่เปลี่ยนพฤติกรรมที่สังเกตได้ จึงไม่มีการทดสอบใดฆ่าได้ การนับ mutant เหล่านี้เป็นความล้มเหลวถือว่าไม่ซื่อสัตย์ NextPDF จึงพิสูจน์และบันทึกลงสมุดบัญชี (ledger) แทนที่จะปัดทิ้งอย่างไม่เป็นทางการ
  • NextPDF ใช้ MSI เพื่อค้นหาและเสริมความแข็งแกร่งให้การทดสอบที่อ่อนแอ โดยใช้เป็นเกตเพื่อการวินิจฉัยใน continuous integration ไม่ใช่ตัวเลขทางการตลาด

การทดสอบแบบกลายพันธุ์บนเอนจินรันผ่าน Infection โดยกำหนดค่าให้ครอบคลุมโครงสร้างต้นทางของ production และเปิดใช้กลุ่มมิวเทเตอร์ด้านเลขคณิต บูลีน ขอบเขตเงื่อนไข ความเท่ากัน ค่าที่คืนกลับ และการลบออก — ตัวดำเนินการเหล่านี้เปิดเผยตรรกะแบบ “ถูกรันแต่ไม่ถูก assert” ได้ตรงจุด ขั้นตอนการทำงานเป็นกลไกชัดเจน:

  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.
ลูปการทดสอบแบบกลายพันธุ์ที่ NextPDF รัน: เริ่มจากการทดสอบที่ขึ้นสีเขียว สร้าง mutant หนึ่งตัว รันการทดสอบที่ครอบคลุมอีกครั้ง แล้วจำแนก mutant ว่าถูกฆ่า (มีการทดสอบจับได้) รอด (ช่องว่างที่มี coverage แต่ไม่มี assertion ซึ่งต้องแก้ไข) หรือพิสูจน์แล้วว่าสมมูล (ไม่มีการทดสอบใดฆ่าได้ บันทึกลง ledger และไม่นับเป็นคะแนนติดลบ)

การตัดสินใจด้านการออกแบบสองข้อทำให้ตัวเลขนี้น่าเชื่อถือ ข้อแรก คะแนนถูกต่อเข้ากับ เกต (gate) ของ continuous integration โดยบังคับใช้ค่า MSI ขั้นต่ำ (และ covered-MSI ขั้นต่ำ) และรันโหมดที่จำกัดขอบเขตตาม diff บนบรรทัดที่เปลี่ยนแปลง ผลคือการเปลี่ยนแปลงที่เพิ่มโค้ดแต่ไม่เพิ่ม assertion ที่แท้จริงจะถูกจับได้ตอนรีวิว ไม่ใช่ถูกพบภายหลัง ข้อสอง NextPDF ไม่ตัด mutant ที่ไม่สะดวกออกไปอย่างเงียบ ๆ mutant ที่ สมมูลเชิงความหมาย อย่างแท้จริง — ตัวอย่างเช่น !== เทียบกับ != เมื่อ strict typing รับประกันว่าตัวถูกดำเนินการทั้งสองมีชนิดเดียวกัน — จะถูกบันทึกลงในสมุดบัญชีการกลายพันธุ์พร้อมการทดสอบที่พิสูจน์ความสมมูลอย่างชัดเจน ผลคือจำนวน mutant ที่รอดสะท้อนช่องว่างจริง ไม่ใช่ผลจากการจัดบัญชีตัวเลข PHPStan Level 10 ร่วมกับ strict_types และพรอเพอร์ตีที่กำหนดชนิดคือสิ่งที่ทำให้การพิสูจน์ความสมมูลเหล่านั้นมีน้ำหนัก

Evidence: Test-backed การทดสอบแบบกลายพันธุ์ได้รับการกำหนดค่าใน เอนจินให้ครอบคลุมไดเรกทอรีต้นทางของ production โดยเปิดใช้กลุ่มมิวเทเตอร์ที่ เปิดเผยพฤติกรรม และบังคับใช้เป็นเกตของ continuous-integration พร้อมค่า MSI ขั้นต่ำและโหมดที่จำกัดขอบเขตตาม diff จึงเป็นการตรวจสอบในขั้น build ไม่ใช่สิ่งที่ เพิ่งนึกขึ้นได้ภายหลัง

Evidence: Test-backed ปัญหา mutant สมมูลได้รับการจัดการ อย่างตรงไปตรงมา mutant ที่สมมูลเชิงความหมายจะถูกจำแนกและรองรับด้วย การทดสอบที่พิสูจน์ความสมมูลโดยเฉพาะในสมุดบัญชีการกลายพันธุ์ โดยความหนักแน่นของ การพิสูจน์แต่ละครั้งอาศัย PHPStan Level 10 ร่วมกับ strict typing จำนวน mutant ที่รอด จึงแทนพฤติกรรมที่ไม่ถูกตรวจจับจริง ไม่ใช่สัญญาณรบกวนที่ฆ่าไม่ได้ซึ่งถูกนับพองจน กลายเป็นคะแนนที่ดูแย่ลง

Evidence: Standard-backed การกลายพันธุ์เป็นเทคนิคที่ได้รับการยอมรับ ไม่ใช่เทคนิคที่ NextPDF สร้างขึ้นเอง Spec: ISO/IEC/IEEE 29119-4, §B.2.4 อธิบายการนำการกลายพันธุ์ทั่วไปไปใช้กับองค์ประกอบของข้อกำหนด เพื่อให้ได้ การกลายพันธุ์เฉพาะสำหรับการทดสอบ เหตุผลที่ต้องมีเทคนิคนี้คือ มาตรฐานเดียวกันระบุว่าการจัดลำดับแบบ subsumes ของเกณฑ์ coverage ไม่ได้ จัดลำดับตามความสามารถในการเปิดเผยข้อบกพร่อง (ISO/IEC/IEEE 29119-4, §C.2.4)

Evidence: Standard-backed coverage เองมีนิยามชัดเจนและ มีข้อจำกัด Spec: PHPUnit แยกความแตกต่างระหว่าง coverage แบบ line, branch และ path ออกจากกัน line coverage บันทึกเพียงว่าบรรทัดที่รันได้บรรทัดหนึ่ง ถูกรัน เท่านั้น การรู้ นิยามนี้ทำให้เห็นความไม่เพียงพอของมันได้อย่างชัดเจน

ประเด็นไม่ได้อยู่ที่คำสั่ง — แต่อยู่ที่สิ่งที่ mutant ที่รอดเผยให้เห็น:

<?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 ที่รอดตัวนั้นคือคุณค่าของกระบวนการทั้งหมด เพราะระบุ assertion ที่ขาดหายไปได้อย่างเจาะจงและเป็นจริง แม้รายงาน coverage จะประเมินว่าส่วนนั้นได้รับการทดสอบครบถ้วนแล้ว

ความเข้าใจผิดหลักคือการมองว่าคะแนนการกลายพันธุ์เป็น เกรด ที่ต้องทำให้สูงที่สุด MSI ที่สูงมากจากการเขียนการทดสอบ เพื่อฆ่า mutant นั้นไร้ความหมายพอ ๆ กับ coverage ที่สูงจากการเรียกเมธอดโดยไม่ assert เมตริกจะถูกบิดเบือนและไม่วัดการตรวจจับอีกต่อไป NextPDF ใช้ MSI เพื่อ ค้นหา การทดสอบที่อ่อนแอ ผลลัพธ์ที่ต้องการคือ assertion ที่ดีขึ้น การโอ้อวดไม่ใช่จุดประสงค์อย่างชัดเจน

ความเข้าใจผิดข้อที่สองคือการมองว่า mutant ที่รอดทุกตัวเป็นข้อบกพร่องของการทดสอบ mutant บางตัวสมมูลอย่างแท้จริงและ ไม่สามารถ ถูกฆ่าได้ด้วยการทดสอบใด ๆ เพราะไม่ได้เปลี่ยนพฤติกรรมที่สังเกตได้ การนับ mutant เหล่านั้นเป็นความล้มเหลวจะทำให้คะแนนไม่ซื่อสัตย์และต่ำเกินจริง อีกทั้งทำให้ผู้คนเคยชินกับการเพิกเฉยต่อรายงาน คำตอบของ NextPDF คือการพิสูจน์ความสมมูลอย่างชัดเจนและบันทึกลง ledger ไม่ใช่การปิดบังอย่างเงียบ ๆ หรือแสร้งทำให้ตัวเลขดูแย่กว่าความเป็นจริง

การทดสอบแบบกลายพันธุ์วัดว่าการทดสอบ ตรวจจับ การเปลี่ยนแปลงที่แทรกเข้ามาได้หรือไม่ แต่ไม่ได้พิสูจน์ว่าโค้ดถูกต้อง ไม่ได้วัดประสิทธิภาพหรือความสอดคล้องตามมาตรฐาน และไม่สามารถฆ่า mutant ที่สมมูลอย่างแท้จริงได้ คะแนนการกลายพันธุ์ปัจจุบัน เกณฑ์ MSI ขั้นต่ำที่บังคับใช้อยู่ จำนวน mutant สมมูลที่บันทึกใน ledger และตัวเลข coverage ใด ๆ ล้วนเป็นสัญญาณคุณภาพที่เปลี่ยนแปลงได้ ซึ่งสร้างจากอาร์ติแฟกต์ของ continuous integration และเผยแพร่ไปพร้อมกับ build ตัวเลขเหล่านี้จงใจไม่ระบุไว้ที่นี่ เพราะตัวเลขที่วางลงในเนื้อความจะล้าสมัยและกลายเป็นคำโกหกเล็ก ๆ ข้อเท็จจริงคงที่เพียงข้อเดียวที่หน้านี้ระบุคือ PHPStan Level 10 ซึ่งเป็นคุณสมบัติของการกำหนดค่าที่รองรับการพิสูจน์ความสมมูล ไม่ใช่การวัดผล

การเลือกมิวเทเตอร์ เกณฑ์ และนโยบาย ledger อยู่ในการกำหนดค่าการกลายพันธุ์ของเอนจินและอาจเปลี่ยนแปลงได้ หากมีความขัดแย้งกับหน้านี้ ให้ถือว่าการกำหนดค่านั้นเป็นแหล่งอ้างอิงที่เชื่อถือได้ ที่นี่ไม่มีการกล่าวอ้างใด ๆ เกี่ยวกับ test effectiveness ของไลบรารีอื่น

  • พีระมิดการทดสอบของ NextPDF — ห้าระดับชั้นที่การทดสอบแบบกลายพันธุ์ตรวจสอบว่าตรวจจับข้อบกพร่องได้จริง
  • Strict types, everywhere — PHPStan Level 10 และ strict typing ทำให้การพิสูจน์ mutant สมมูลมีความหนักแน่นได้อย่างไร
  • Golden-file testing — อีกชั้นหนึ่งที่การทดสอบแบบกลายพันธุ์ช่วยตรวจสอบความสามารถในการตรวจจับ
  • Mutant — การเปลี่ยนแปลงเล็ก ๆ ที่จงใจเพียงรายการเดียวในโค้ดต้นทาง ใช้เพื่อทดสอบว่าชุดทดสอบสังเกตเห็นการเปลี่ยนแปลงนั้นหรือไม่
  • Killed mutant — mutant ที่ทำให้การทดสอบล้มเหลวอย่างน้อยหนึ่งรายการ แสดงว่าพฤติกรรมนั้นถูก assert ไว้จริง
  • Escaped mutant — mutant ที่ทำให้การทดสอบทุกตัวยังคงผ่าน พฤติกรรมถูกรันแต่ไม่เคยถูก assert — เป็นจุดอ่อนที่ต้องแก้ไข
  • Equivalent mutant — mutant ที่ไม่เปลี่ยนพฤติกรรมที่สังเกตได้ จึงไม่มีการทดสอบใดฆ่าได้ NextPDF พิสูจน์และบันทึก mutant เหล่านี้ลง ledger
  • MSI (Mutation Score Indicator) — โดยคร่าว ๆ คือจำนวน mutant ที่ถูกฆ่าหารด้วยจำนวน mutant ที่ไม่สมมูลทั้งหมด เป็นการวัดการตรวจจับ ไม่ใช่การรันโค้ด
  • Line coverage — เมตริกที่บันทึกเพียงว่าบรรทัดที่รันได้บรรทัดหนึ่งถูกรันระหว่างการรันชุดทดสอบเท่านั้น นิยามโดย PHPUnit และไม่เพียงพอในตัวเอง