ארבעה סוגים נפוצים של כיסוי קוד

מהי כיסוי קוד ואילו דרכים נפוצות יש למדוד אותו?

שמעת את הביטוי code coverage (כיסוי קוד)? בפוסט הזה נבדוק מהו כיסוי הקוד בבדיקות ונציע ארבע דרכים נפוצות למדוד אותו.

כיסוי הקוד הוא מדד שמודד את אחוז קוד המקור שהבדיקות מבצעות. כך תוכלו לזהות אזורים שבהם יכול להיות שלא בוצעו בדיקות מתאימות.

לעיתים קרובות, תיעוד המדדים האלה נראה כך:

קובץ % דוחות % 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);
}

כיסוי שורות הוא המדד של אחוז שורות הקוד שניתן להריץ שמערכת הבדיקה הפעילה. אם שורת קוד לא מופעלת, סימן שחלק מהקוד לא נבדק.

בדוגמה של הקוד יש שמונה שורות של קוד הפעלה (מודגש באדום ובירוק), אבל הבדיקות לא מבצעות את התנאי 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 במשפטים מותנים.

בדוגמה לקוד יש חמישה ענפים:

  1. להתקשר אל calcCoffeeIngredient רק באמצעות coffeeName סימן וי.
  2. שיחה אל calcCoffeeIngredient עם coffeeName ועם cup סימן וי.
  3. הקפה הוא אספרסו סימן וי.
  4. הקפה הוא אמריקנו סימן X.
  5. קפה אחר סימן צ'ק.

הבדיקות מכסות את כל ההסתעפויות מלבד התנאי 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% מהקוד, אבל כל הבדיקות לא משמעותיות, יכול להיות שתקבלו תחושה מוטעית של ביטחון שהקוד נבדק היטב. אם תמחקו או תנתקו בטעות חלק מקוד האפליקציה, הבדיקות עדיין יעברו בהצלחה, למרות שהאפליקציה לא פועלת יותר בצורה תקינה.

כדי להימנע מהתרחיש הזה:

  • בדיקת הבדיקה. כתבו ובדקו את הבדיקות כדי לוודא שהן משמעותיות, ולבדוק את הקוד במגוון תרחישים.
  • כדאי להשתמש בכיסוי הקוד כקו מנחה, ולא בתור המדד היחיד ליעילות של הבדיקה או לאיכות הקוד.

שימוש בכיסוי קוד בסוגים שונים של בדיקות

נבחן לעומק איך אפשר להשתמש במדד כיסוי הקוד עם שלושת סוגי הבדיקות הנפוצים:

  • בדיקות יחידה. אלה סוגי הבדיקות הטובים ביותר לצורך איסוף כיסוי קוד, כי הם נועדו לכסות כמה תרחישי בדיקה ומסלולי בדיקה קטנים.
  • בדיקות שילוב. הם יכולים לעזור לכם לאסוף כיסוי קוד לבדיקות השילוב, אבל חשוב להשתמש בהם בזהירות. במקרה כזה, מחשבים את הכיסוי של חלק גדול יותר מקוד המקור, ויכול להיות שיהיה קשה לקבוע אילו בדיקות מכסות אילו חלקים בקוד. עם זאת, חישוב של כיסוי הקוד של בדיקות שילוב עשוי להיות שימושי במערכות מדור קודם שאין בהן יחידות מבודדות.
  • בדיקות מקצה לקצה (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({});
  });
});