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

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

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

מה זה כיסוי של קוד?

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

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

קובץ % דוחות הסתעפות באחוזים % פונקציות % שורות קווים לא מוסתרים
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 {};
}
…

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

בדוגמת הקוד יש חמש הסתעפויות:

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

הבדיקות כוללות את כל ההסתעפויות למעט התנאי Coffee is Americano. לכן, הכיסוי של ההסתעפות הוא 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({});
  });
});