تغطية التعليمات

تقيس تغطية التعليمات (Statement Coverage) عدد التعليمات القابلة للتنفيذ في البرنامج التي شغّلتها مجموعة من الاختبارات مرة واحدة على الأقل. وفي اختبار التعليمات (Statement Testing)، وهو تقنية الصندوق الأبيض التي يقوم عليها هذا القياس، تكون بنود التغطية هي التعليمات القابلة للتنفيذ، فتصمّم الاختبارات حتى تُنفَّذ كلها أو نسبة متفق عليها منها.

مثال: دالة فيها ⁦6⁩ تعليمات قابلة للتنفيذ، واختبارك الوحيد يشغّل ⁦5⁩ منها. فتغطية التعليمات ⁦5 / 6 = 83%⁩. واختبار ثانٍ يصل إلى التعليمة السادسة يرفعها إلى ⁦100%⁩: فكل تعليمة نُفّذت مرة واحدة على الأقل، وهذا كل ما يَعِد به هذا المعيار.

لماذا يهم

لا يستطيع العيب أن يسبب إخفاقاً (Failure) إلا حين تُنفَّذ الشيفرة التي تحتويه. وتشير تغطية التعليمات إلى الشيفرة التي لم يشغّلها أي اختبار قط، وأيّ عيب فيها لا يمكن أن يظهر في تلك الاختبارات. وهي أيضاً أبسط مقاييس التغطية شرحاً.

لكن ضعفها مهم بالقدر نفسه. فيمكن بلوغ ⁦100%⁩ منها باختبارات قليلة جداً تترك نتائج القرارات وحالات الحلقات وقيم البيانات دون اختبار؛ وتغطية الفروع (Branch Coverage)، موضوع الدرس التالي، معيار أقوى منها. والمختبِر الذي يعرف بدقة ما يثبته هذا الرقم يستطيع أن يستخدمه لإيجاد الثغرات، دون أن يعدّه دليلاً على أن الشيفرة مختبرة جيداً.

ما الذي تقيسه تغطية التعليمات

وفق منهج ISTQB للمستوى التأسيسي (الإصدار ⁦4.0⁩)، تُقاس تغطية التعليمات بعدد التعليمات القابلة للتنفيذ (Executable statements) التي نفّذتها الاختبارات مقسوماً على العدد الكلي للتعليمات القابلة للتنفيذ، معبّراً عنه بنسبة مئوية: statement coverage = statements executed / executable statements × 100%.

التعليمة القابلة للتنفيذ هي تعليمة تؤدي عملاً عند تشغيل البرنامج. وتُحتسب كل تعليمة مرة واحدة، مهما كان عدد الاختبارات التي شغّلتها ومهما كرّرتها حلقة. وعند ⁦100%⁩ تكون كل تعليمة قابلة للتنفيذ قد نُفّذت مرة واحدة على الأقل، فالتعليمة التي تحوي عيباً قد نُفّذت أيضاً، وهذا قد يسبب إخفاقاً يكشف العيب. وكلمة «قد» هي المفتاح هنا، كما يبيّن المثال التطبيقي.

  • تُحتسب: عمليات الإسناد والتصريحات التي تحمل قيمة (let fee = 5;)، واستدعاءات الدوال، وجمل return وthrow، وكل جملة if أو switch أو حلقة بحد ذاتها، لأن تقييم شرطها عمل يُنفَّذ.
  • لا تُحتسب عند العدّ اليدوي: التعليقات، والأسطر الفارغة، والأسطر التي لا تحوي إلا قوساً معقوصاً، وترويسة الدالة (Signature)، والشيفرة الخاصة بالأنواع فقط مثل interface في TypeScript.
  • تُعدّ التعليمة مغطاة إذا نفّذها اختبار واحد على الأقل، سواء نجح ذلك الاختبار أم لا، وسواء تحقق من شيء أم لا.
  • تختلف الأدوات في التفاصيل: فأداة coverage.py تسجّل الأسطر المنفَّذة، وسطر def في Python يُنفَّذ عند استيراد الوحدة البرمجية (Module)، فيُعدّ مغطى حتى لو لم يستدعِ أي اختبار تلك الدالة. لذا لا تقارن النسب إلا إذا جاءت من الأداة نفسها والإعدادات نفسها والملفات نفسها.

مثال تطبيقي: دالة رسوم المواقف

المتطلب: «الوقوف مجاني خلال أول ⁦30⁩ دقيقة. وبعد ذلك تُحتسب ⁦5.00⁩ ريالات عن كل ساعة أو جزء من ساعة. ويدفع الأعضاء النصف». كتب المطوّر الدالة أدناه واختباري وحدة، T1 وT2؛ والدالة Math.ceil في السطر ⁦4⁩ تقرّب إلى الأعلى، وبها يتحقق شرط «كل ساعة أو جزء من ساعة». وتعليماتها القابلة للتنفيذ هي الأسطر ⁦2⁩ و⁦3⁩ و⁦4⁩ و⁦6⁩ و⁦7⁩ و⁦9⁩؛ أما ترويسة الدالة وأسطر أقواس الإغلاق فليست تعليمات.

للدالة parkingFee ست تعليمات قابلة للتنفيذ: يشغّل T1 خمساً منها (⁦83%⁩) ويشغّل T2 الست كلها (⁦100%⁩)، ومع ذلك لا تُفشل الرسوم الابتدائية الخاطئة في التعليمة ⁦1⁩ أياً من الاختبارين.

الدالة parkingFee واختبارا الوحدة الخاصان بها وتغطية التعليمات

 1  function parkingFee(minutes, member) {
 2    الجزء 1:let fee = 5;
 3    الجزء 2:if (minutes > 30) {
 4      الجزء 3:fee = Math.ceil((minutes - 30) / 60) * 5;
 5    }
 6    الجزء 4:if (member) {
 7      الجزء 5:fee = fee / 2;
 8    }
 9    الجزء 6:return fee;
10  }

Executable statements: lines 2, 3, 4, 6, 7, 9 (6 in total)

Unit test                  Expected  Actual  Result  Lines run      Statement coverage
T1  parkingFee(90, false)  5         5       passed  2 3 4 6 9      5/6   83%
T2  parkingFee(200, true)  7.5       7.5     passed  2 3 4 6 7 9    6/6  100%
T1 + T2                                              2 3 4 6 7 9    6/6  100%
  • 1–6بنود التغطية الستة: مقام المعادلة
  1. 1التعليمة ⁦1⁩ · السطر ⁦2⁩تُنفَّذ في T1 وT2. وفيها العيب، أي رسوم ابتدائية ⁦5⁩ بدلاً من ⁦0⁩، لكن السطر ⁦4⁩ يستبدلها في الاختبارين، فلا يفشل أي منهما.T1 · T2تنبيه
  2. 2التعليمة ⁦2⁩ · السطر ⁦3⁩: قرارتُنفَّذ في الاختبارين، ونتيجتها true في المرتين. أما نتيجتها false فلا تعليمة لها، فلا تطلبها تغطية التعليمات أبداً.T1 · T2
  3. 3التعليمة ⁦3⁩ · السطر ⁦4⁩تُنفَّذ في الاختبارين: فكلٌّ من ⁦90⁩ و⁦200⁩ دقيقة أكثر من ⁦30⁩.T1 · T2
  4. 4التعليمة ⁦4⁩ · السطر ⁦6⁩: قرارتُنفَّذ في الاختبارين: نتيجتها false في T1 وtrue في T2.T1 · T2
  5. 5التعليمة ⁦5⁩ · السطر ⁦7⁩تُنفَّذ في T2 وحده، وهو اختبار العضو. ومع T1 وحده تكون التغطية ⁦5 / 6 = 83%⁩.T2
  6. 6التعليمة ⁦6⁩ · السطر ⁦9⁩تُنفَّذ في الاختبارين.T1 · T2

ترويسة الدالة وأسطر أقواس الإغلاق ليست تعليمات قابلة للتنفيذ، فلا تدخل في البسط ولا في المقام.

  • الاختبار T1 parkingFee(90, false) يشغّل الأسطر ⁦2⁩ و⁦3⁩ و⁦4⁩ و⁦6⁩ و⁦9⁩ ويُعيد ⁦5⁩ كما هو متوقع. ولا يُنفَّذ السطر ⁦7⁩ لأن قيمة member هي false: ⁦5 / 6 = 83%⁩.
  • الاختبار T2 parkingFee(200, true) يشغّل التعليمات الست كلها: ⁦170⁩ دقيقة بعد الفترة المجانية تعني ساعتين وجزءاً من ساعة ثالثة، فتُحتسب ⁦3⁩ ساعات، أي ⁦15.00⁩ ريالاً، ونصفها ⁦7.50⁩. ومعاً يبلغ T1 وT2 ⁦6 / 6 = 100%⁩، وT2 وحده يبلغها أيضاً.
  • اختبار من الجملة الأولى في المتطلب، T3 parkingFee(20, false)، يتوقع ⁦0⁩ ويحصل على ⁦5⁩: فالسطر ⁦2⁩ يبدأ الرسوم من ⁦5⁩ بدلاً من ⁦0⁩، فتُحتسب رسوم على الفترة المجانية.
  • نُفّذ السطر ⁦2⁩ في T1 وT2 كليهما، ومع ذلك لم يفشل أي منهما: فالسطر ⁦4⁩ استبدل القيمة الخاطئة في كل مرة. ولا يظهر العيب إلا حين تكون نتيجة minutes > 30 هي false، وهي نتيجة لا تعليمة خاصة بها، فلم تطلبها تغطية التعليمات قط. أما تغطية الفروع في الدرس التالي فتطلبها.
النسخة النصية
 1  function parkingFee(minutes, member) {
 2    let fee = 5;
 3    if (minutes > 30) {
 4      fee = Math.ceil((minutes - 30) / 60) * 5;
 5    }
 6    if (member) {
 7      fee = fee / 2;
 8    }
 9    return fee;
10  }

Executable statements: lines 2, 3, 4, 6, 7, 9 (6 in total)

Unit test                  Expected  Actual  Result  Lines run      Statement coverage
T1  parkingFee(90, false)  5         5       passed  2 3 4 6 9      5/6   83%
T2  parkingFee(200, true)  7.5       7.5     passed  2 3 4 6 7 9    6/6  100%
T1 + T2                                              2 3 4 6 7 9    6/6  100%

ما لا تثبته تغطية التعليمات بنسبة ⁦100%⁩

يذكر منهج ISTQB حدّين: تنفيذ التعليمة لا يكشف كل عيب فيها، كالعيب الذي يعتمد على البيانات، وتغطية التعليمات بنسبة ⁦100%⁩ لا تضمن اختبار منطق القرارات كله. ويظهر هذان الحدّان في أشكال متكررة:

  • النتائج الغائبة: جملة if بلا else لها نتيجة false لا تعليمة خاصة بها. وهناك يقع عيب المواقف.
  • العيوب المرتبطة بالبيانات: التعليمة average = total / count تُنفَّذ دون إخفاق حين تكون قيمة count هي ⁦3⁩، وتسبّب إخفاقاً أو تُعيد قيمة بلا معنى حين تكون ⁦0⁩. والتعليمة مغطاة في الحالتين.
  • الحلقات: مرور واحد في جسم حلقة بسيطة يغطي كل تعليماته. أما عدم الدخول إلى الحلقة أصلاً (قائمة فارغة) أو تكرارها مرات كثيرة فلا يتطلبهما المعيار أبداً.
  • الشروط المركّبة: if (gold || total >= 200) تعليمة واحدة، يغطيها اختبار تكون فيه قيمة gold هي true، وفي معظم اللغات لا يُقيَّم الشرط الثاني عندئذ أصلاً. ويعود درس تغطية الشروط إلى هذه المسألة.

كيف تستخدم تغطية التعليمات استخداماً جيداً

تغطية التعليمات أنفع ما تكون حدّاً أدنى: فالتعليمة التي لا يشغّلها أي اختبار نقطة عمياء مؤكدة. وأقل ما تنفع حين تصبح هدفاً، لأن أرخص طريقة لرفعها اختبار يشغّل الشيفرة ولا يتحقق إلا من القليل.

  • ابدأ باختبارات مبنية على المتطلبات ومصمَّمة بتقسيم التكافؤ (Equivalence Partitioning) وتحليل القيم الحدّية (Boundary Value Analysis)، ثم قِس التغطية، وبعدها فقط أضف اختبارات للتعليمات التي لم تُنفَّذ. فتقسيم دقائق الوقوف إلى «حتى ⁦30⁩» و«أكثر من ⁦30⁩» يُنتج الاختبار T3، الذي لم يطلبه أي تقرير تغطية.
  • حين لا يمكن الوصول إلى تعليمة في بيئة الاختبار، تستطيع الفرق استبعادها صراحةً، كما في التعليق # pragma: no cover في coverage.py أو /* istanbul ignore next */ في Istanbul. وراجع كل استبعاد، لأن الشيفرة المستبعدة لا تُقاس بعد ذلك إطلاقاً.

جرّبها: مولّد أقسام التكافؤ جرّبها: مولّد القيم الحدّية

أخطاء شائعة

  • احتساب التعليقات أو الأقواس أو ترويسة الدالة تعليمات، فيتغير المقام.
  • فهم تغطية التعليمات بنسبة ⁦100%⁩ على أنها «كل المسارات مختبرة». فهي لا تعني إلا أن كل تعليمة نُفّذت مرة واحدة، ربما ببيانات غير ضارة ومع النتيجة نفسها لكل قرار في كل مرة.
  • كتابة اختبار لمجرد تنفيذ تعليمة غير مغطاة، دون تأكيد (Assertion) أو بنتيجة متوقعة منسوخة من الشيفرة.
  • احتساب التعليمة مرتين لأن اختبارين شغّلاها أو لأن حلقة كرّرتها: فالبسط يعدّ التعليمات المختلفة.
  • استبعاد الشيفرة التي يصعب الوصول إليها دون مراجعة، فترتفع النسبة ويبقى الخطر.

من منظور ضمان الجودة

نادراً ما يحسب المختبِر تغطية التعليمات يدوياً في العمل، فالأداة تفعل ذلك. لكن حسابها يدوياً بضع مرات يعلّمك معنى الرقم، فتقرأ التقارير بعين ناقدة: أيّ التعليمات لم تُنفَّذ قط، وأيّ قرار يتحكم في الوصول إليها، وهل يصفها متطلب. وحين يُبلغ مطوّر عن «تغطية تعليمات ⁦100%⁩»، فاسأل: أيّ الاختبارات أنتجت هذا الرقم، وبماذا تتحقق، وأيّ نتائج القرارات وحالات البيانات لم تمرّ بها قط؟

وفي الفريق، اتفقوا على المعيار الذي يستخدمه هدف التغطية وعلى الشيفرة التي يشملها، وسجّلوا ذلك في خطة الاختبار أو في بوابة الجودة (Quality gate) في خط CI، وأبلغوا عنه مع نطاقه: «اختبارات الوحدة، تغطية التعليمات لوحدة الرسوم: ⁦100%⁩؛ نتيجة false لفحص الفترة المجانية بلا اختبار». وللمنطق عالي المخاطر اطلبوا تغطية الفروع أيضاً.

تمرين عملي

قِس تغطية التعليمات لدالة التقييم

المطلوب

المتطلب: «تعرض صفحة المنتج متوسط تقييم العملاء بمنزلة عشرية واحدة. والمنتجات التي يبلغ متوسطها ⁦4.5⁩ فأكثر تعرض Top rated بدلاً منه. والمنتجات التي لا تقييمات لها تعرض No ratings yet». وفي الأسفل الدالة واختبار الوحدة الوحيد الخاص بها؛ وtoFixed(1) تحوّل العدد إلى نص بمنزلة عشرية واحدة.

عُدّ التعليمات القابلة للتنفيذ، واحسب تغطية التعليمات للاختبار T1، وأضف اختباراً واحداً يرفع المجموعة إلى ⁦100%⁩، ثم تحقق مما يتركه المتطلب دون اختبار حتى عندئذ.

المادة
 1  function averageRating(ratings) {
 2    let sum = 0;
 3    for (const r of ratings) {
 4      sum = sum + r;
 5    }
 6    const avg = sum / ratings.length;
 7    if (avg >= 4.5) {
 8      return 'Top rated';
 9    }
10    return avg.toFixed(1);
11  }

Unit test                   Expected      Actual        Result
T1  averageRating([5, 4])   'Top rated'   'Top rated'   passed
الخطوات
  1. اكتب التعليمات القابلة للتنفيذ بأرقام أسطرها.
  2. حدّد التعليمات التي يشغّلها T1 واحسب تغطيته.
  3. أضف اختباراً واحداً يبلغ مع T1 تغطية ⁦100%⁩، مع نتيجة متوقعة مأخوذة من المتطلب.
  4. جِد جملة المتطلب التي لا يغطيها أي اختبار، وتتبّعها عبر الشيفرة، وقرّر هل ستنجح.
إظهار الإجابة النموذجية
الإجابة النموذجية

التعليمات القابلة للتنفيذ: الأسطر ⁦2⁩ و⁦3⁩ و⁦4⁩ و⁦6⁩ و⁦7⁩ و⁦8⁩ و⁦10⁩، أي سبع تعليمات.

الاختبار T1 (المجموع ⁦9⁩ والمتوسط ⁦4.5⁩) يعود من السطر ⁦8⁩. فهو يشغّل الأسطر ⁦2⁩ و⁦3⁩ و⁦4⁩ و⁦6⁩ و⁦7⁩ و⁦8⁩ دون السطر ⁦10⁩: ⁦6 / 7 = 86%⁩.

أضف T2 averageRating([3, 4]): المتوسط ⁦3.5⁩، والمتوقع 3.5. وهو يشغّل السطر ⁦10⁩، فيبلغ T1 وT2 معاً ⁦7 / 7 = 100%⁩. ولا يستطيع اختبار واحد بمفرده بلوغها: فالسطران ⁦8⁩ و⁦10⁩ جملتا return بديلتان.

لا اختبار يغطي جملة «المنتجات التي لا تقييمات لها تعرض No ratings yet». تتبّع averageRating([]): لا يُنفَّذ جسم الحلقة ولا مرة، وsum / ratings.length تساوي 0 / 0، وهي NaN (ليست عدداً) في JavaScript، والشرط NaN >= 4.5 غير متحقق، فيُعيد السطر ⁦10⁩ النص 'NaN'. المتوقع No ratings yet والفعلي 'NaN': يفشل الاختبار. فالقاعدة لم تُبرمَج أصلاً، والسطر ⁦6⁩ الذي غطّاه الاختباران لا يخطئ إلا مع هذه البيانات.

إذن عند تغطية تعليمات ⁦100%⁩ بقيت القائمة الفارغة (عدم الدخول إلى الحلقة) والقسمة على صفر ومتطلب مفقود كلها دون اختبار. وهناك سؤال لمالك المنتج أيضاً: المتوسط ⁦4.46⁩ يُعرض 4.5 دون الشارة. فأيّ القيمتين تعتمدها القاعدة؟

أسئلة شائعة

ما النسبة الجيدة لتغطية التعليمات؟

لا يوجد رقم عام، ومنهج ISTQB لا يحدد رقماً. تضع الفرق حدّاً بحسب المخاطر وتطبّقه على الشيفرة المهمة، وعلى الشيفرة المتغيرة في الغالب. ونسبة إجمالية عالية مع تعليمات حرجة غير مغطاة أسوأ من نسبة أقل تكون فيها الشيفرة غير المغطاة معروفة ومقبولة.

كم اختباراً أحتاج لبلوغ تغطية تعليمات بنسبة ⁦100%⁩؟

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

هل تعني تغطية الفروع بنسبة ⁦100%⁩ تغطية التعليمات بنسبة ⁦100%⁩؟

نعم. ينص منهج ISTQB على أن تغطية الفروع بنسبة ⁦100%⁩ تحقق تغطية التعليمات بنسبة ⁦100%⁩، والعكس غير صحيح: فمثال المواقف يبلغ تغطية تعليمات ⁦100%⁩ دون أن يمرّ قط بنتيجة false للشرط minutes > 30. ويشرح الدرس التالي السبب.

المراجع

  • ISTQB Certified Tester Foundation Level (CTFL) v4.0ISTQB · 2023المنهج التأسيسي لاختبار البرمجيات: مستويات الاختبار وأنواعه وتقنيات تصميم الاختبارات وإدارة الاختبار والأدوات.
  • ISTQB GlossaryISTQBالتعريفات القياسية لمصطلحات الاختبار — حالة الاختبار واختبار الانحدار والخطورة والأولوية وتحليل القيم الحدّية وغيرها.
  • NIST SP 500-235: Structured Testing: A Testing Methodology Using the Cyclomatic Complexity MetricNIST · 1996الاختبار المهيكل (اختبار المسارات الأساسية): استخدام مقياس التعقيد الدوري لـ McCabe لاشتقاق مجموعة أساسية من المسارات المستقلة لاختبارها — وهو أشمل من تغطية التعليمات والفروع.

تدرّب بأدوات متقن

اشتق فئات التكافؤ الصالحة وغير الصالحة لكل مدخل واختر قيم اختبار تمثيلية.

تصميم الاختبارات

ولّد القيم الحدّية (min-1 وmin وmin+1 والقيمة الاسمية وmax-1 وmax وmax+1) للقيود الرقمية وقيود الطول.

تصميم الاختبارات

حوّل قائمة متطلبات مرقّمة إلى حالات اختبار قابلة للتتبّع مع مصفوفة تربط المتطلبات بالاختبارات.

تصميم الاختبارات

للتعمّق أكثر

تقدّمك في هذا الموضوع

جزء من مسار تعلم ضمان الجودة