أربعة أنواع شائعة لتغطية الرمز

تعرَّف على تغطية الرمز البرمجي واكتشِف أربع طرق شائعة لقياسها.

هل سمعت عبارة "تغطية الرمز"؟ في هذه المشاركة، سنتعرّف على تغطية الرموز البرمجية في الاختبارات وأربع طرق شائعة لقياسها.

ما هي تغطية التعليمات البرمجية؟

تغطية الرمز هي مقياس يقيس النسبة المئوية لرمز المصدر الذي تنفذه اختباراتك. ويساعدك ذلك في تحديد الجوانب التي قد لا يتم اختبارها بشكل صحيح.

في أغلب الأحيان، يكون تسجيل هذه المقاييس على النحو التالي:

ملفّ النسب المئوية للبيانات الفرع% وظائف النسبة المئوية النسبة المئوية للخطوط الخطوط غير المشمولة
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 أو الحلقات. يحدِّد ما إذا كانت الاختبارات تفحص كلا فرعَي العبارة الشَرطية، وهما "صواب" و"خطأ".

هناك خمسة فروع في مثال الرمز البرمجي:

  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 عدم وجود أخطاء أو عيوب في الرمز البرمجي.

طريقة غير مجدية لتحقيق تغطية كاملة للرموز البرمجية

ضع في اعتبارك الاختبار التالي:

/* 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 للدوالّ والسطور والفروع والتعليمات، ولكنّه لا معنى له لأنّه لا يختبر الرمز البرمجي فعليًا. سيسري تأكيد expect(true).toBe(true) دائمًا بغض النظر عمّا إذا كان الرمز يعمل بشكل صحيح.

المقياس السيئ أسوأ من عدم استخدام أي مقياس

يمكن أن يمنحك مقياس سيئ إحساسًا زائفًا بالأمان، وهو أسوأ من عدم توفّر أي مقياس على الإطلاق. على سبيل المثال، إذا كانت لديك مجموعة اختبارات تحقّق تغطية بنسبة %100 للرمز البرمجي ولكنّ الاختبارات جميعها غير ذات معنى، قد تشعر بأمان زائف بأنّ رمزك البرمجي قد تم اختباره جيدًا. إذا حذفت جزءًا من رمز التطبيق أو أفسدته عن طريق الخطأ، سيظلّ التطبيق يجتاز الاختبارات، حتى إذا لم يعُد يعمل بشكل صحيح.

لتجنب هذا السيناريو:

  • مراجعة الاختبار اكتب الاختبارات وراجِعها للتأكّد من أنّها ذات معنى، واختَبِر الرمز في مجموعة متنوعة من السيناريوهات المختلفة.
  • استخدِم تغطية الرمز البرمجي كدليل إرشادي، وليس كمقياس وحيد لفعالية الاختبار أو جودة الرمز البرمجي.

استخدام تغطية الرمز البرمجي في أنواع مختلفة من الاختبارات

لنلقِ نظرة أقرب على كيفية استخدام تغطية الرمز البرمجي مع الأنواع الثلاثة الشائعة من الاختبارات:

  • اختبارات الوحدة: وهي أفضل أنواع الاختبارات لجمع تغطية الرمز البرمجي لأنّها مصمّمة لتغطية سيناريوهات ومسارات اختبار صغيرة متعددة.
  • اختبارات الدمج: ويمكن أن تساعد في جمع تغطية الرمز لاختبارات الدمج، ولكن عليك استخدامها بحذر. في هذه الحالة، يتم احتساب تغطية جزء أكبر من رمز المصدر، وقد يكون من الصعب تحديد الاختبارات التي تغطي أجزاء من الرمز البرمجي. ومع ذلك، قد يكون احتساب تغطية الرمز البرمجي لاختبارات الدمج مفيدًا للأنظمة القديمة التي لا تحتوي على وحدات معزولة بشكل جيد.
  • الاختبارات الشاملة بين الأطراف: إنّ قياس تغطية الرمز البرمجي لاختبارات 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({});
  });
});