מהי כיסוי קוד ואילו ארבע דרכים נפוצות יש למדוד אותו?
שמעתם את הביטוי 'כיסוי קוד'? במאמר הזה נסביר מהי כיסוי קוד בבדיקות ואת ארבע הדרכים הנפוצות למדוד אותו.
מהו כיסוי קוד?
כיסוי הקוד הוא מדד שמודד את אחוז קוד המקור שהבדיקות מבצעות. כך תוכלו לזהות אזורים שלא בוצעו בהם בדיקות מתאימות.
לרוב, התיעוד של המדדים האלה נראה כך:
קובץ | % דוחות | % Branch | % פונקציות | % שורות | קווים לא מכוסים |
---|---|---|---|---|---|
file.js | 90% | 100% | 90% | 80% | 89,256 |
coffee.js | 55.55% | 80% | 50% | 62.5% | 10-11, 18 |
כשמוסיפים תכונות ובדיקות חדשות, הגדלת אחוזי הכיסוי של הקוד יכולה להעניק לכם יותר ביטחון שהאפליקציה נבדקה בצורה יסודית. אבל יש עוד דברים שאפשר לגלות.
ארבעה סוגים נפוצים של כיסוי קוד
יש ארבע דרכים נפוצות לאיסוף ולחישוב של כיסוי הקוד: כיסוי פונקציות, כיסוי שורות, כיסוי ענפים ותכנים.
כדי לראות איך מחושב האחוז של כל סוג כיסוי קוד, אפשר להיעזר בדוגמת הקוד הבאה לחישוב המרכיבים של קפה:
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
return {};
}
export function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
הבדיקות שמאמתות את הפונקציה calcCoffeeIngredient
הן:
/* coffee.test.js */
import { describe, expect, assert, it } from 'vitest';
import { calcCoffeeIngredient } from '../src/coffee-incomplete';
describe('Coffee', () => {
it('should have espresso', () => {
const result = calcCoffeeIngredient('espresso', 2);
expect(result).to.deep.equal({ espresso: 60 });
});
it('should have nothing', () => {
const result = calcCoffeeIngredient('unknown');
expect(result).to.deep.equal({});
});
});
אתם יכולים להריץ את הקוד והבדיקות בהדגמה הזו או לבדוק את המאגר.
כיסוי פונקציות
כיסוי הקוד: 50%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
// ...
}
function isValidCoffee(name) {
// ...
}
כיסוי פונקציות הוא מדד פשוט. הוא מתעד את אחוז הפונקציות בקוד שאליו הקריאות של הבדיקות מגיעות.
בדוגמת הקוד יש שתי פונקציות: calcCoffeeIngredient
ו-isValidCoffee
. הבדיקות קוראות רק לפונקציה calcCoffeeIngredient
, כך שכיסוי הפונקציה הוא 50%.
כיסוי קו
כיסוי הקוד: 62.5%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
return {};
}
export function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
כיסוי שורות הוא המדד של אחוז שורות הקוד שניתן להריץ ש-JUnit הפעיל. אם שורת קוד לא מופעלת, סימן שחלק מהקוד לא נבדק.
דוגמת הקוד כוללת שמונה שורות של קוד שניתן להריץ (מודגשות באדום ובירוק), אבל הבדיקות לא מריצות את התנאי americano
(שתי שורות) ואת הפונקציה isValidCoffee
(שורה אחת). התוצאה היא כיסוי קו של 62.5%.
שימו לב שבדוחות כיסוי שורות לא נלקחות בחשבון הצהרות, כמו function isValidCoffee(name)
ו-let espresso, water;
, כי הן לא ניתנות להרצה.
כיסוי ההסתעפויות
כיסוי קוד: 80%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
// ...
if (coffeeName === 'espresso') {
// ...
return { espresso };
}
if (coffeeName === 'americano') {
// ...
return { espresso, water };
}
return {};
}
…
כיסוי ההסתעפויות: המדד הזה מודד את אחוז ההסתעפויות או נקודות ההחלטה שבקוד שהופעלו, כמו משפטי if או לולאות. הוא קובע אם הבדיקות בודקות גם את ההסתעפות של הערך True וגם את ההסתעפות של הערך False במשפטים מותנים.
בדוגמה לקוד יש חמישה ענפים:
- שיחה אל
calcCoffeeIngredient
באמצעותcoffeeName
בלבד - שיחה אל
calcCoffeeIngredient
עםcoffeeName
ו-cup
- הקפה הוא אספרסו
- הקפה הוא אמריקנו
- קפה אחר
הבדיקות מכסות את כל ההסתעפויות מלבד התנאי Coffee is Americano
. כך ש-branch coverage הוא 80%.
מה נכלל בדוח
כיסוי הקוד: 55.55%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
return {};
}
export function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
כיסוי הצהרות הוא המדד של אחוז ההצהרות בקוד שהבדיקות מבצעות. במבט ראשון, יכול להיות שתתהו "האם זה לא אותו הדבר כמו כיסוי שורות?" אכן, כיסוי הצהרות דומה לכיסוי שורות, אבל הוא מתייחס לשורות בודדות של קוד שמכילות כמה הצהרות.
בדוגמת הקוד יש שמונה שורות של קוד שניתן להריץ, אבל יש תשע הצהרות. רואים את השורה שמכילה שתי הצהרות?
espresso = 30 * cup; water = 70 * cup;
הבדיקות מכסות רק חמש מתוך תשע ההצהרות, ולכן הכיסוי של ההצהרות הוא 55.55%.
אם תמיד כותבים משפט אחד בכל שורה, הכיסוי של השורה יהיה דומה לכיסוי של המשפט.
איזה סוג של כיסוי קוד כדאי לבחור?
רוב הכלים לבדיקת כיסוי הקוד כוללים את ארבעת הסוגים הנפוצים הבאים של כיסוי קוד. הבחירה של מדד הכיסוי של הקוד שצריך לתת לו עדיפות תלויה בדרישות הספציפיות של הפרויקט, בשיטות הפיתוח וביעדי הבדיקה.
באופן כללי, כיסוי ההצהרות הוא נקודת מוצא טובה כי מדובר במדד פשוט וקליל להבנה. בניגוד לכיסוי הצהרות, כיסוי ההסתעפויות וכיסוי הפונקציות מודדים אם הבדיקות מפעילות תנאי (הסתעפות) או פונקציה. לכן, הם מהווים המשך טבעי אחרי כיסוי ההצהרות.
אחרי שתשיגו כיסוי גבוה של דפי חשבון, תוכלו להמשיך לכיסוי של סניפים וכיסוי של פונקציות.
האם כיסוי הבדיקה זהה לכיסוי הקוד?
לא. לעיתים קרובות מתבלבלים בין כיסוי הבדיקה לבין כיסוי הקוד, אבל הם שונים:
- כיסוי הבדיקה: מדד איכותי שמודד את מידת הכיסוי של חבילה הבדיקה לתכונות התוכנה. הוא עוזר לקבוע את רמת הסיכון.
- כיסוי קוד: מדד כמותי שמודד את היחס של הקוד שמתבצע במהלך הבדיקה. מדובר בכמות הקוד שהבדיקות מכסות.
הנה אנלוגיה פשוטה: נניח שאפליקציית אינטרנט היא בית.
- כיסוי הבדיקה מודד את מידת הכיסוי של הבדיקה בחדרים בבית.
- כיסוי הקוד מודד את החלק היחסי של הבית שהבדיקות עברו בו.
כיסוי קוד של 100% לא אומר שאין באגים
כמובן שרצוי להגיע לכיסוי קוד גבוה בבדיקה, אבל כיסוי קוד של 100% לא מבטיח שאין באגים או פגמים בקוד.
דרך לא משמעותית להשגת כיסוי קוד של 100%
נבחן את הבדיקה הבאה:
/* coffee.test.js */
// ...
describe('Warning: Do not do this', () => {
it('is meaningless', () => {
calcCoffeeIngredient('espresso', 2);
calcCoffeeIngredient('americano');
calcCoffeeIngredient('unknown');
isValidCoffee('mocha');
expect(true).toBe(true); // not meaningful assertion
});
});
הבדיקה הזו מספקת כיסוי של 100% לפונקציות, לשורות, להסתעפויות ולמשפטים, אבל היא לא הגיונית כי היא לא בודקת את הקוד בפועל. טענת הנכוֹנוּת (assertion) של expect(true).toBe(true)
תמיד תעבור, ללא קשר לכך שהקוד פועל כראוי.
מדד לא טוב גרוע יותר ממצב שבו אין מדד
מדד שגוי יכול לתת לכם תחושת ביטחון מוטעית, וזו תחושה גרועה יותר מאשר לא להשתמש במדד בכלל. לדוגמה, אם יש לכם חבילת בדיקות שמכסה 100% מהקוד, אבל כל הבדיקות לא משמעותיות, יכול להיות שתקבלו תחושת ביטחון מוטעית שהקוד שלכם נבדק היטב. אם תמחקו בטעות חלק מקוד האפליקציה או תגרמו לו נזק, הבדיקות עדיין יעברו, גם אם האפליקציה כבר לא תפעל כראוי.
כדי להימנע מהתרחיש הזה:
- בדיקת הבדיקה כותבים ובודקים בדיקות כדי לוודא שהן משמעותיות, ובודקים את הקוד במגוון תרחישים שונים.
- השתמשו במדד הכיסוי של הקוד כקו מנחה, ולא כמדד היחיד של יעילות הבדיקה או איכות הקוד.
שימוש במדד כיסוי הקוד בסוגים שונים של בדיקות
נבחן לעומק איך אפשר להשתמש במדד כיסוי הקוד עם שלושת סוגי הבדיקות הנפוצים:
- בדיקות יחידה (unit testing) אלה סוגי הבדיקות הטובים ביותר לצורך איסוף כיסוי קוד, כי הם נועדו לכסות כמה תרחישים קטנים ונתיבי בדיקה.
- בדיקות אינטגרציה הם יכולים לעזור לכם לאסוף כיסוי קוד לבדיקות השילוב, אבל חשוב להשתמש בהם בזהירות. במקרה כזה, מחשבים את הכיסוי של חלק גדול יותר מקוד המקור, ויכול להיות שיהיה קשה לקבוע אילו בדיקות מכסות אילו חלקים בקוד. עם זאת, חישוב הכיסוי של הקוד בבדיקות השילוב עשוי להיות שימושי במערכות מדור קודם שאין בהן יחידות מבודדות היטב.
- בדיקות מקצה לקצה (E2E). קשה למדוד את כיסוי הקוד בבדיקות E2E בגלל אופי הבדיקות המורכב. במקום להשתמש במדד כיסוי הקוד, עדיף להשתמש במדד כיסוי הדרישות. הסיבה לכך היא שהמטרה של בדיקות E2E היא לכסות את הדרישות של הבדיקה, ולא להתמקד בקוד המקור.
סיכום
כיסוי הקוד יכול להיות מדד שימושי למדידת היעילות של הבדיקות. הוא יכול לעזור לכם לשפר את האיכות של האפליקציה על ידי הבטחה שהלוגיקה החשובה בקוד נבדקה היטב.
עם זאת, חשוב לזכור שכיסוי הקוד הוא רק מדד אחד. חשוב להביא בחשבון גם גורמים אחרים, כמו איכות הבדיקות והדרישות של האפליקציה.
המטרה היא לא לכסות 100% מהקוד. במקום זאת, כדאי להשתמש בבדיקה של כיסוי הקוד בשילוב עם תוכנית בדיקה מקיפה שכוללת מגוון שיטות בדיקה, כולל בדיקות יחידה, בדיקות אינטגרציה, בדיקות מקצה לקצה ובדיקות ידניות.
אפשר לעיין בקטע הקוד המלא ובבדיקות עם כיסוי קוד טוב. אפשר גם להריץ את הקוד והבדיקות באמצעות הדגמה פעילה הזו.
/* coffee.js - a complete example */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
if (!isValidCoffee(coffeeName)) return {};
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
throw new Error (`${coffeeName} not found`);
}
function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
/* coffee.test.js - a complete test suite */
import { describe, expect, it } from 'vitest';
import { calcCoffeeIngredient } from '../src/coffee-complete';
describe('Coffee', () => {
it('should have espresso', () => {
const result = calcCoffeeIngredient('espresso', 2);
expect(result).to.deep.equal({ espresso: 60 });
});
it('should have americano', () => {
const result = calcCoffeeIngredient('americano');
expect(result.espresso).to.equal(30);
expect(result.water).to.equal(70);
});
it('should throw error', () => {
const func = () => calcCoffeeIngredient('mocha');
expect(func).toThrowError(new Error('mocha not found'));
});
it('should have nothing', () => {
const result = calcCoffeeIngredient('unknown')
expect(result).to.deep.equal({});
});
});