False Kill ใน Mutation Testing: Baseline การเข้าถึงไฟล์ และเหตุผลที่ Kill
เผยแพร่ 2026-10-27
Mutation testing คือการเปลี่ยนโค้ดของคุณโดยตั้งใจ (เช่น < กลายเป็น <= หรือ return x กลายเป็น return None) แล้วตรวจว่าเทสต์ของคุณจับความเปลี่ยนแปลงได้หรือไม่ มิวแทนต์ที่ "killed" หมายถึงมีเทสต์ล้มเหลว แต่เทสต์อาจล้มเหลวด้วยเหตุผลที่ไม่เกี่ยวกับการกลายพันธุ์เลย และเมื่อเป็นเช่นนั้น คะแนน mutation ของคุณจะดูดีกว่าคุณภาพเทสต์จริง คู่มือนี้แสดงสามการตรวจสอบที่จับ false kill เหล่านี้ได้
ทำไมจึงเกิด false kill
| สาเหตุ | สิ่งที่คุณเห็น |
|---|---|
| ชุดเทสต์ล้มเหลวหรือไม่เสถียรอยู่แล้วก่อนทำ mutation | มี kill จำนวนมากที่เกิดจากข้อผิดพลาดเดียวกันที่ไม่เกี่ยวข้อง |
| เทสต์ import ไฟล์ต้นฉบับ ไม่ใช่สำเนาที่ถูกกลายพันธุ์ | kill เกิดจาก timeout หรือข้อผิดพลาดของสภาพแวดล้อมเท่านั้น |
| Timeout จากเครื่องที่ช้า | kill กระจุกตัวอยู่ที่เทสต์ที่รันนาน |
| สถานะที่ใช้ร่วมกันระหว่างการรัน (ไฟล์ชั่วคราว แคช พอร์ต) | ผล kill เปลี่ยนเมื่อรันมิวแทนต์ตัวเดิมซ้ำ |
Timeout มีผลต่อคะแนน: ใน Stryker, detected = killed + timeout และ mutation score = detected / valid × 100 เครื่องที่ timeout บ่อยจะทำให้คะแนนสูงเกินจริง ที่มา: Stryker mutant states and metrics ตรวจสอบเมื่อ 2026-10-19
การตรวจที่ 1 — Baseline บนสำเนาที่สะอาด
- คัดลอกโปรเจกต์ไปยังไดเรกทอรีใหม่ ให้เหมือนกับที่เครื่องมือ mutation จะทำทุกประการ (sandbox เดียวกัน ตัวแปรสภาพแวดล้อมเดียวกัน)
- รันชุดเทสต์ทั้งหมดที่นั่นโดยไม่ทำ mutation สามครั้ง
- ต้องผ่านทั้งสามครั้ง ความล้มเหลวใด ๆ ตรงนี้คือความล้มเหลวที่มีอยู่ก่อนแล้ว ซึ่งจะปรากฏเป็น kill ปลอมในภายหลัง ให้แก้หรือแยกกักกันก่อน
cp -r project /tmp/mt-baseline && cd /tmp/mt-baseline
for i in 1 2 3; do pytest -q -p no:cacheprovider || echo "BASELINE FAIL $i"; done
การตรวจที่ 2 — พิสูจน์ว่าเทสต์อ่านไฟล์ที่ถูกกลายพันธุ์
หากเทสต์ไม่เคยโหลดไฟล์ที่ถูกกลายพันธุ์ ก็ไม่สามารถ kill มิวแทนต์นั้นได้ ให้บันทึกการเข้าถึงไฟล์ระหว่างการรันมิวแทนต์แต่ละตัว ใน Python ใช้ audit hook ก็เพียงพอ:
# conftest.py
import sys, os
TARGET = os.environ.get("MUT_TARGET") # path of the mutated file
def hook(event, args):
if event == "open" and TARGET and str(args[0]).endswith(TARGET):
open("/tmp/mut_touched", "a").write("1")
sys.addaudithook(hook)
รันมิวแทนต์แต่ละตัวโดยลบ bytecode เก่าออก (ลบ __pycache__ ในสำเนา) เพื่อให้ไฟล์ต้นฉบับถูกอ่านจริง กฎคือ: kill ที่ไฟล์เป้าหมายไม่เคยถูกเปิดให้ทำเครื่องหมายเป็น suspect แล้วรันซ้ำ สำหรับสแตกอื่น strace -e trace=openat (Linux) หรือข้อมูล coverage ของตัวรันเทสต์ใช้ได้เช่นกัน
การตรวจที่ 3 — ดูการกระจายของเหตุผลที่ kill
- สำหรับมิวแทนต์ที่ถูก kill ทุกตัว ให้เก็บเหตุผลที่การรันล้มเหลว: assertion error, exception อื่น, timeout, crash, ข้อผิดพลาดตอน collection/import
- ทำตารางนับอย่างง่าย
| เหตุผล | รูปแบบที่ปกติ | สัญญาณเตือน |
|---|---|---|
| Assertion ล้มเหลว | kill ส่วนใหญ่ | — |
| Exception อื่น | บ้าง | ข้อความเดียวกันซ้ำในมิวแทนต์จำนวนมาก |
| Timeout | น้อย (มิวแทนต์ที่ทำให้เกิดลูปไม่รู้จบ) | สัดส่วนมาก เพิ่มขึ้นบนเครื่องที่ช้ากว่า |
| ข้อผิดพลาดตอน import / collection | แทบไม่มี | สัดส่วนที่มีนัยสำคัญ ไม่ว่าเท่าไร แปลว่าตัวชุดทดสอบ ไม่ใช่เทสต์ ที่กำลังล้มเหลว |
สรุปรวมกัน
- Baseline ผ่านเขียว 3/3 บนสำเนาที่สะอาด
- ทุก kill มีบันทึก "เปิดไฟล์เป้าหมาย"
- Assertion failure เป็นเหตุผลที่ kill มากที่สุด
- สุ่มรัน kill ซ้ำ 5% ผลไม่ควรเปลี่ยน
คำถามที่พบบ่อย
- ควรเพิ่ม timeout หรือไม่
- ตั้งจาก baseline: วัดเวลารันชุดเทสต์ปกติ แล้วให้เวลาเป็นจำนวนเท่าของค่านั้น ตั้งต่ำเกินไปจะเกิด kill ปลอม สูงเกินไปจะทำให้รันช้า
- ข้อผิดพลาดตอนคอมไพล์นับด้วยหรือไม่
- ใน Stryker ข้อผิดพลาดตอนคอมไพล์และตอนรันไทม์ถือเป็นมิวแทนต์ "invalid" และไม่นับรวมในคะแนน
แหล่งที่มา
- Stryker: สถานะและตัวชี้วัดของมิวแทนต์ — ตรวจสอบเมื่อ 2026-10-19
ที่เกี่ยวข้อง (ภาษาเกาหลี): รวมความรู้การพัฒนา (개발 지식 모음)
🌐 English · Español · Bahasa Indonesia · 日本語 · ไทย · Tiếng Việt · 中文