ทำความเข้าใจการทดสอบแบบกลายพันธุ์
Spec: ISO/IEC/IEEE 29119-4 ISO/IEC/IEEE 29119-4 Spec: PHPUnit 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 ไม่ใช่ตัวเลขทางการตลาด
NextPDF จัดการเรื่องนี้อย่างไร
หัวข้อที่มีชื่อว่า “NextPDF จัดการเรื่องนี้อย่างไร”การทดสอบแบบกลายพันธุ์บนเอนจินรันผ่าน Infection โดยกำหนดค่าให้ครอบคลุมโครงสร้างต้นทางของ production และเปิดใช้กลุ่มมิวเทเตอร์ด้านเลขคณิต บูลีน ขอบเขตเงื่อนไข ความเท่ากัน ค่าที่คืนกลับ และการลบออก — ตัวดำเนินการเหล่านี้เปิดเผยตรรกะแบบ “ถูกรันแต่ไม่ถูก assert” ได้ตรงจุด ขั้นตอนการทำงานเป็นกลไกชัดเจน:
- Start green The suite must pass before mutation begins.
- Mutate Apply one small, deliberate change to the source.
- Re-run Run the tests that cover the mutated line.
- Killed A test failed — the behaviour is genuinely asserted.
- Escaped All tests still pass — a weak spot to strengthen.
- Equivalent No test can kill it because behaviour is unchanged — proven and ledgered, not scored as a miss.
การตัดสินใจด้านการออกแบบสองข้อทำให้ตัวเลขนี้น่าเชื่อถือ ข้อแรก คะแนนถูกต่อเข้ากับ เกต (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 ISO/IEC/IEEE 29119-4 §B.2.4 อธิบายการนำการกลายพันธุ์ทั่วไปไปใช้กับองค์ประกอบของข้อกำหนด เพื่อให้ได้ การกลายพันธุ์เฉพาะสำหรับการทดสอบ เหตุผลที่ต้องมีเทคนิคนี้คือ มาตรฐานเดียวกันระบุว่าการจัดลำดับแบบ subsumes ของเกณฑ์ coverage ไม่ได้ จัดลำดับตามความสามารถในการเปิดเผยข้อบกพร่อง (ISO/IEC/IEEE 29119-4, §C.2.4)
Evidence: Standard-backed coverage เองมีนิยามชัดเจนและ มีข้อจำกัด Spec: PHPUnit 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 gatemutant ที่รอดตัวนั้นคือคุณค่าของกระบวนการทั้งหมด เพราะระบุ 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 และไม่เพียงพอในตัวเอง