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

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

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

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

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

في كثير من الأحيان، يبدو تسجيل هذه المقاييس كما يلي:

ملفّ بيانات النسبة المئوية النسبة المئوية للفرع النسبة المئوية للدوال نسبة الأسطر الخطوط غير المغطاة
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 من عدم وجود أخطاء أو عيوب في الرمز.

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

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

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