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

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

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

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

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

غالبًا ما يظهر تسجيل هذه المقاييس على النحو التالي:

ملفّ بيانات النسبة المئوية الفرع% النسبة المئوية للدوال نسبة السطور الخطوط غير المغطاة
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 فقط، وبالتالي تكون تغطية الدالة P.

تغطية الخط

تغطية الرمز: b.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;، لأنّها غير قابلة للتنفيذ.

تغطية الفروع

تغطية الرمز: �

/* 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% لا تضمن غياب الأخطاء أو العيوب في التعليمات البرمجية.

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

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

/* 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({});
  });
});