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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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