בדיקות מוטציה הן שליליות. בדיקות TDD הן חיוביות.
לשתיהן קוראים בדיקות, אבל הן טוענות דברים הפוכים. בדיקת TDD אומרת מה המערכת חייבת לעשות. ריצת מוטציות יכולה רק לדווח מה מערך הבדיקות שלכם לא שם לב אליו. לדעת איזה סימן אתם מחזיקים משנה מה לעשות עם התוצאה.
שני סוגי טענה
בדיקה מוכוונת בדיקות נכתבת לפני שההתנהגות קיימת. היא נכשלת, אחר כך עוברת, ומאז היא טענה קבועה: הקלט הזה חייב לייצר את התוצאה ההיא. מערך הבדיקות מצטבר למפרט שאפשר לקרוא, והכתיבה מראש מפעילה לחץ על העיצוב, כי קוד שקשה לקרוא לו קשה לבדוק. ריצת מוטציות עושה משהו אחר במבנה שלו. היא לוקחת קוד שכבר עובד, משנה אותו בכוונה, ומריצה שוב את המערך. כל תוצאה מנוסחת בשלילה: השינוי הזה לא נתפס. שום דבר בפלט הזה לא אומר בשביל מה התוכנה קיימת.
מערך TDD ירוק הוא אוסף של טענות על כוונה. ציון מוטציה גבוה הוא היעדר של סוג מסוים של הוכחה נגד מערך הבדיקות שלכם. רק אחד מהשניים הוא מפרט.
הבדיקה החיובית
- + נכתבת לפני הקוד, כך שהדרישה קיימת במילים לפני שהיא קיימת בלוגיקה
- + נכשלת קודם, וזו ההוכחה הזולה היחידה שהבדיקה בכלל מסוגלת להיכשל
- + מנסחת כלל, ולכן שורדת שינויי מבנה: הפנימיות יכולות לזוז בלי שהטענה תזוז
- + מפעילה לחץ על העיצוב בזמן שהעיצוב עוד זול לשינוי
- + נקראת כתיעוד בידי האדם הבא, כולל הסוכן הבא
הבדיקה השלילית
- − רצה בדיעבד, על קוד ועל בדיקות שכבר קיימים
- − מוטנט שנהרג לא מוסיף מידע; רק שורדים נושאים אות
- − שורד הוא עדות לעיוורון, ולא עדות לפגם במוצר
- − לא אומרת דבר על דרישות חסרות, רק על חוסר רגישות לשינויים שנוסו
- − מייצרת דוח ורשימת מטלות, לא נכס שמישהו שומר
אותה מילה, שני מכשירים שונים
| הבדיקה החיובית | הבדיקה השלילית | |
|---|---|---|
| מה היא טוענת | המערכת חייבת להתנהג כך. | מערך הבדיקות שלכם לא שם לב לשינוי הזה. |
| מה ירוק מוכיח | כל התנהגות שאופיינה עד כה ממומשת. | המערך רגיש לקבוצת האופרטורים הזו. שום דבר על נכונות. |
| מה נשאר אחריה | בדיקות, ועיצוב שעוצב בגלל הצורך לכתוב אותן. | דוח. צריך לחלץ ממנו את הערך לפני שהוא מתיישן. |
| מתי היא רצה | כל הזמן, בקצב של דקות, בזמן כתיבת הקוד. | ב-CI: הדרגתית על קבצים שהשתנו, או כסריקה לילית. |
| מודל העלות | משולם בהתמדה ונטמע בתוך הפיתוח. | מוטנטים כפול זמן הרצת הבדיקות הרלוונטיות. גדל בשניהם. |
| השפעה על העיצוב | לחץ של יכולת בדיקה. עיצוב מסורבל כואב מיד. | אין. היא מדרגת את מה שכבר קיים. |
| איך הכישלון מגיע | אדום, בכוונה, בכתיבתכם, צעד אחר צעד. | שורד שאף אחד לא כתב, ושצריך כעת לפרש. |
אותו שורד, שתי תגובות
כאן הסימן מפסיק להיות פילוסופיה. דוח המוטציות מוסר לכם מקום; מה שתכתבו אחר כך יקבע אם המערך יתחזק או רק יתקשח.
הקוד, והמוטנט ששורד
jsהסרת .trim() היא מוטציה סטנדרטית של קריאה למתודה. היא שורדת בכל פעם שאין fixture עם רווחים בקצוות, כלומר ברוב ה-fixtures.
// importer.js
const normalise = (value) => value.trim().toLowerCase();
export function importRows(rows) {
return rows.map((row) => ({ email: normalise(row.email) }));
}
// Surviving mutant: normalise() with .trim() removed.
// The suite passes either way, so the report flags it. להרוג את המוטנט, ולצמד את המערך
jsהבדיקה הזו הורגת את השורד. היא גם מקפיאה את המימוש הנוכחי: normalise חייבת להישאר נגישה ולהיקרא בדיוק כך. שנו את שמה, שלבו אותה פנימה או העבירו אותה מעבר לגבול, והבדיקה תישבר בעוד ההתנהגות לא השתנתה בכלל.
it('calls normalise once per row', () => {
const spy = vi.spyOn(internals, 'normalise');
importRows([{ email: ' Ada@Example.COM ' }]);
expect(spy).toHaveBeenCalledTimes(1);
});
// Green. Mutant dead. Score up.
// Nothing here states what an imported email address should look like. לענות על השאלה שהשורד שאל
jsאותו מוטנט, אותה הריגה, אבל הטענה היא כלל שגם מנהל מוצר יכול לקרוא. היא עוברת דרך הפונקציה הציבורית, כך שהפנימיות נשארות חופשיות לזוז, והיא מסבירה את עצמה בהודעת הכישלון גם בעוד חצי שנה.
it('trims and lower-cases every imported email address', () => {
expect(importRows([{ email: ' Ada@Example.COM ' }]))
.toEqual([{ email: 'ada@example.com' }]);
});
// Same mutant dead, but the suite gained a specification
// instead of a snapshot of today's call graph. לשמור את הביקורת זולה מספיק כדי להמשיך להריץ
jsonניתוח כיסוי לכל בדיקה הוא מנוף הביצועים הגדול ביותר: רצות רק הבדיקות שמגיעות בפועל לכל מוטנט. מצב הדרגתי משאיר ריצת pull request בטווח של דקות. ערך ה-break הוא רצפה נגד נסיגה, לא יעד לרדוף אחריו.
{
"testRunner": "vitest",
"coverageAnalysis": "perTest",
"incremental": true,
"mutate": ["src/**/*.js", "!src/**/*.test.js"],
"thresholds": { "high": 80, "low": 60, "break": 60 }
} איפה השלילי הופך רעיל
לרדוף אחרי הציון
מהרגע שציון המוטציה הוא יעד, נכתבות בדיקות שהורגות מוטנטים במקום בדיקות שמנסחות דרישות. הן עוברות סקירה כי הן ירוקות, והן בדיוק אלו שנשברות בשינוי המבנה הבא, למרות שההתנהגות לא השתנתה.
להיאבק במוטנטים שקולים
יש מוטציות שמשנות את הקוד בלי לשנות התנהגות נצפית. שום בדיקה כנה לא יכולה להרוג אותן. תעדו, החריגו, והמשיכו. הזמן כאן קונה מספר, לא ביטחון.
לטעון את המוטציה במקום את הכלל
אם אתם לא מצליחים לנסח את הדרישה שבדיקה חדשה מגנה עליה, כתבתם גלאי שינויים. הוא ידווח על כל עריכה עתידית ככישלון וילמד את הצוות להפסיק לקרוא את פלט הבדיקות.
להריץ הכול, כל פעם
סריקה מלאה על בסיס קוד גדול היא עבודה לילית, לא שער של pull request. כוונו את ריצת ה-PR לקבצים שהשתנו עם ניתוח כיסוי לכל בדיקה, והשאירו את הריצה המלאה מחוץ לנתיב הקריטי.
להפוך כל שורד לטענה חיובית
לקרוא את השורד כשאלה
- - איזו התנהגות הייתה צריכה להיות נכונה כדי שהשינוי הזה ישבור משהו?
- - איזו דרישה, אם מישהו היה כותב אותה, הייתה נכשלת עכשיו?
- - מי במורד הזרם היה מרגיש אם המוטציה הזו הייתה מגיעה לפרודקשן?
לכתוב את התשובה, לא את ההריגה
- - לקרוא לבדיקה על שם הכלל, לא על שם המוטנט או מספר השורה
- - לטעון דרך הממשק הציבורי כדי שהפנימיות יישארו חופשיות להשתנות
- - אם אפשר להגיע לכלל רק בחשיפת הפנימיות, זה ממצא עיצובי ולא ממצא בדיקות
או למחוק את הקוד
- - אם אין דרישה שניתן לנסח, שום דבר לא באמת תלוי בהתנהגות הזו
- - שורד בקוד לא מאופיין הוא לרוב דרישה מתה ולא בדיקה חסרה
- - מחיקה היא ההריגה הזולה ביותר, והיא גם מזרזת את הביקורת הבאה
אחת כותבת את המפרט, השנייה מבקרת אותו
בדיקות מוטציה אינן TDD משופר ואינן תחליף לו. הן לא מייצרות בדיקות, לא עיצוב ולא הצהרת כוונה. מה שהן מייצרות הוא רשימת מקומות שבהם המפרט שלכם פחות רגיש ממה שהנחתם, וזה ערך אמיתי וגם דבר אחר לגמרי.
לכן שמרו על חלוקת העבודה מפורשת. בדיקות נולדות דרך TDD, כטענות חיוביות על התנהגות. ריצות מוטציה מתרחשות מדי פעם כביקורת על הטענות האלה. ואף שורד לא הולך ישר לבדיקה: קודם מתרגמים אותו לדרישה, או מוחקים את הקוד שבו הוא יושב. ממצא שלילי הופך לערך מתמשך רק כשמישהו הופך אותו לטענה חיובית על מה שהתוכנה קיימת בשבילו.
מאמרי הנדסה קשורים
לכיסוי ולמורכבות יש מדד משל עצמם, וכבר יש תהליך עבודה עם סוכנים שמעביר את ההקשחה לתפקיד נפרד.
מדד CRAP: מורכבות וכיסוי במספר אחד
איך נוסחת CRAP משלבת מורכבות עם כיסוי, מה כל ציון דורש, ואיך להפוך את זה לשער בלי לפתוח אפוס ניקוי.
SwarmForge: מה נחיל הסוכנים של אנקל בוב מבין נכון
ביקורת על פירוק התפקידים, בידוד ה-worktree ופרוטוקול ההעברה של SwarmForge, ואיזה חלקים עוברים לכל צוות.
שאלות נפוצות
האם בדיקות מוטציה טובות יותר מכיסוי קוד?
הן עונות על שאלה חזקה יותר במחיר גבוה בהרבה. כיסוי סופר אם שורה הורצה; מוטציה שואלת אם משהו בכלל נבדק. השתמשו בכיסוי כשער זול ומתמשך, ובבדיקות מוטציה כביקורת תקופתית על קוד שחשוב.
לאיזה ציון מוטציה כדאי לשאוף?
ציונים מעל 80 אחוז נחשבים בדרך כלל חזקים, ו-60 עד 80 אחוז סבירים עם פערים אמיתיים, אבל למספר יש משמעות רק ברמת מודול. כלל שימושי יותר הוא בלי נסיגה בקבצים שהשתנו, עם ערך break שעוצר את הידרדרות הציון.
האם בדיקות מוטציה מחליפות TDD?
לא. בדיקות מוטציה לא כותבות דבר ולא מאפיינות דבר. הן מדרגות את המערך שכבר קיים, וכל ממצא עוד דורש החלטה אנושית לגבי איזו דרישה הוא מרמז עליה.
באילו כלים צוותים משתמשים?
PIT הוא הבחירה המבוססת ב-JVM, Stryker Mutator מכסה JavaScript, TypeScript, C# ו-Scala, וב-Python מקובלים mutmut ו-cosmic-ray. כולם מאפשרים לצמצם ריצה לקבצים שהשתנו, וזה מה שהופך את הפרקטיקה לכדאית.
איפה בדיקות מוטציה משתלבות ב-CI?
הדרגתית ב-pull requests, מוגבלת לקבצים שהשתנו עם ניתוח כיסוי לכל בדיקה, ובנוסף סריקה מלאה לפי לוח זמנים. דווחו על שורדים כפריטי סקירה ולא ככשלי בנייה, ושימו שער רק על נסיגה בציון.