تغطية الفروع والقرارات

تقيس تغطية القرارات (Decision Coverage) عدد نتائج القرارات في الشيفرة التي مرّت بها اختباراتك: جانبا كل if، وكل case في switch، ونتيجتا الحلقة «كرّر مرة أخرى» و«اخرج». أما تغطية الفروع (Branch Coverage)، التي يدرّسها منهج ISTQB للمستوى التأسيسي (الإصدار ⁦4.0⁩) بدلاً منها، فتقيس فروع تدفق التحكم، وتطلب في الشيفرة المعتادة الاختبارات نفسها.

مثال: الجملة if (member) { discountPercent = 20; } لها نتيجتان. اختبار واحد بعميل عضو يشغّل كل التعليمات، لكن نتيجة غير العضو، حيث لا يُنفَّذ شيء داخل if، لا تُجرَّب أبداً. والمعياران كلاهما يتطلبان اختباراً ثانياً لها.

لماذا يهم

لا ترى تغطية التعليمات (Statement Coverage)، موضوع الدرس السابق، النتيجة التي يتخطى فيها التنفيذ جزءاً من الشيفرة: فما إن تُنفَّذ التعليمات داخل if حتى لا تسأل أبداً عمّا يحدث حين تُتخطّى. وكثير من العيوب يسكن هناك بالضبط: متغير يحتفظ بقيمة افتراضية خاطئة حين لا يُؤخذ جانب if، أو else مفقودة، أو مُدخل يُفترض أن يستبعده شرط حارس مثل if (qty > 0) لكن لا يرسله أي اختبار. وتفرض تغطية الفروع اختباراً يمرّ بكل نتيجة من هذه النتائج.

وبالنسبة إلى المختبِر، كل نتيجة غير مغطاة سؤال ملموس: ماذا يجب أن يحدث لغير العضو، أو لسلة فارغة، أو لحالة غير معروفة؟ والإجابة تصبح اختباراً نتيجته المتوقعة مأخوذة من المتطلب، أو تكشف عيباً أو متطلباً ناقصاً.

القرارات ونتائجها

القرار (Decision) نقطة تختار فيها الشيفرة التعليمة التي تُنفَّذ بعدها، وكل اختيار ممكن هو نتيجة قرار (Decision Outcome). وتغطية القرارات هي عدد نتائج القرارات التي نُفّذت مقسوماً على العدد الكلي لنتائج القرارات، بنسبة مئوية. ابدأ بإيجاد كل القرارات، فبعضها يسهل أن يفوتك:

  • if مع else أو بدونها: نتيجتان. وبدون else تتخطى نتيجة false الكتلة، وليس لها أي تعليمة خاصة بها.
  • العامل الشرطي a ? b : c: نتيجتان داخل تعليمة واحدة، فتعدّها تغطية التعليمات وتغطية الأسطر مغطّاةً حتى لو لم يُنفَّذ أحد جانبيها.
  • الحلقات (for وwhile): الدخول إلى الجسم أو الخروج. والاختبار الذي يشغّل الجسم مرة واحدة يأخذ النتيجتين، لذا لا تُطلب حالة صفر تكرارات أبداً.
  • switch: نتيجة لكل case، إضافةً إلى النتيجة الافتراضية، حتى لو لم تُكتب عبارة default.
  • if (a && b) قرار واحد بنتيجتين؛ أما الشروط داخله فهي موضوع الدرس التالي.
  • الشيفرة المضغوطة أو المصغَّرة (minified) تخفي القرارات: نسّقها أولاً بحيث تقع كل تعليمة، ومنها كل if وكل حلقة، في سطر مستقل، ثم ابحث داخل التعابير عن ?:.

جرّبها: منسّق JavaScript

تغطية الفروع وعلاقتها بتغطية القرارات

يدرّس منهج ISTQB للمستوى التأسيسي (الإصدار ⁦4.0⁩) اختبار الفروع (Branch Testing). والفرع (Branch) انتقال للتحكم بين عقدتين في مخطط تدفق التحكم (Control Flow Graph)، أي المخطط الذي يبيّن الترتيبات الممكنة لتنفيذ التعليمات، وعُقده تعليمات أو كتل من التعليمات: وهو غير مشروط في الشيفرة المتتابعة، ومشروط حين يكون نتيجة قرار. وتغطية الفروع هي عدد الفروع المنفَّذة مقسوماً على العدد الكلي للفروع، بنسبة مئوية.

ولأن المعيارين يعدّان أشياء مختلفة، قد تختلف نسبتاهما دون ⁦100%⁩، لكن الاختبارات نفسها تبلغ ⁦100%⁩ في كليهما في الشيفرة المعتادة: فما إن تُنفَّذ كل نتيجة قرار حتى تكون كل كتلة بعدها قد نُفّذت أيضاً. وكثير من الفرق يستخدم الاسمين على أنهما مترادفان؛ وقد درّس المنهج في الإصدار ⁦3.1⁩ تغطية القرارات، ثم استبدل بها الإصدار ⁦4.0⁩ تغطية الفروع.

  • تعرض أدوات التغطية «الفروع» وتقصد في الغالب نتائج القرارات؛ فأداة coverage.py مثلاً تقيسها عند تشغيلها بالخيار --branch.
  • تختلف الأدوات فيما تعدّه: فبعضها يعدّ أيضاً كل طرف من أطراف && و||، أو كل قفزة في الشيفرة المترجمة. لذا لا تقارن النسب إلا من الأداة نفسها.

لماذا تتضمن تغطية الفروع الكاملة تغطية التعليمات الكاملة

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

والعكس غير صحيح. فنتيجة false لجملة if بلا else لا تحتوي أي تعليمة، لذا يمكن للاختبارات أن تشغّل كل التعليمات دون أن تمرّ بها أبداً.

  • التضمّن حكم يخص نسبة ⁦100%⁩ وحدها. ودونها قد تكون أيٌّ من النسبتين هي الأعلى: فالاختبار الذي يمرّ بجانب true فقط من if بلا else قد يحقق ⁦100%⁩ من التعليمات و⁦50%⁩ من نتائج القرارات، أما الاختبار الذي يمرّ بالجانب القصير فقط من if/else جانبها الآخر طويل فيحقق ⁦50%⁩ من نتائج القرارات وحصة أقل بكثير من التعليمات.
  • الأقوى لا يعني الكافي: تغطية الفروع الكاملة لا تقول شيئاً عن توليفات النتائج، ولا عن العيوب المعتمدة على البيانات مثل القسمة على صفر، ولا عن شيفرة لم تُكتب أصلاً.

مثال تطبيقي: خصم لم يطلبه أحد

المتطلب: «يحصل الأعضاء على خصم ⁦20%⁩ من السعر. ويدفع غير الأعضاء السعر كاملاً. والطلبات التي تقل عن ⁦200.00⁩ ريال بعد الخصم تدفع ⁦25.00⁩ ريالاً للشحن؛ والطلبات بقيمة ⁦200.00⁩ ريال فأكثر شحنها مجاني». في الشيفرة أدناه ⁦8⁩ تعليمات قابلة للتنفيذ وقراران، في السطرين ⁦3⁩ و⁦8⁩: أي ⁦4⁩ نتائج قرارات.

  • الاختبار T1 checkout(100, true) يتوقع ⁦80 + 25 = 105⁩ وينجح. وهو يشغّل التعليمات الثماني كلها (تغطية تعليمات ⁦100%⁩)، لكنه لا يأخذ إلا نتيجة true من كل قرار: ⁦2⁩ من ⁦4⁩، أي تغطية قرارات ⁦50%⁩.
  • الناقص: السطر ⁦3⁩ بنتيجة false (عميل غير عضو) والسطر ⁦8⁩ بنتيجة false (إجمالي ⁦200.00⁩ أو أكثر). واختبار واحد يأخذ الاثنتين: T2 checkout(300, false)، ويتوقع ⁦300⁩.
  • يفشل T2: تعيد الشيفرة ⁦270⁩، لأن السطر ⁦2⁩ يبدأ كل عميل بخصم ⁦10%⁩، وهي قيمة افتراضية لا يذكرها المتطلب. فيحصل غير الأعضاء على خصم لا يستحقونه، وبعض طلباتهم التي تزيد قليلاً على ⁦200.00⁩ تفقد الشحن المجاني بسببه.
  • مع T1 وT2 تصبح تغطية القرارات ⁦100%⁩. والفريق الذي توقف عند تغطية تعليمات ⁦100%⁩ بعد T1 كان سيُطلق العيب.
  • أي قيمة على كل جانب تكفي للمعيار؛ ويضيف تحليل القيم الحدّية إجماليين ⁦199.99⁩ و⁦200.00⁩ للتحقق من المقارنة <. وبعد الإصلاح أعد تشغيلها جميعاً بوصفها اختبار تأكيد (Confirmation Testing).
 1  function checkout(price: number, member: boolean): number {
 2    let discountPercent = 10;
 3    if (member) {
 4      discountPercent = 20;
 5    }
 6    const total = price - (price * discountPercent) / 100;
 7    let shipping = 0;
 8    if (total < 200) {
 9      shipping = 25;
10    }
11    return total + shipping;
12  }

Test                        Expected  Actual  Result  Outcomes taken
T1  checkout(100, true)     105       105     passed  line 3 true,  line 8 true
T2  checkout(300, false)    300       270     failed  line 3 false, line 8 false

Coverage    Statements    Decision outcomes   Not taken
T1          8/8  100%     2/4   50%           line 3 false, line 8 false
T1 + T2     8/8  100%     4/4  100%           none
اختباران يغطيان نتائج القرارات الأربع في دالة checkout: يأخذ T1 نتيجتَي true ويشغّل وحده كل التعليمات، ويأخذ T2 نتيجتَي false ويفشل بسبب الخصم الافتراضي ⁦10%⁩.
سؤال 1: السطر ⁦3⁩: if (member)القرار الأول: نتيجتان
  • الإجابة: نتيجة true: السطر ⁦4⁩ يجعل الخصم ⁦20%⁩

    سؤال 2: السطر ⁦8⁩: if (total < 200)القرار الثاني: الإجمالي ⁦80⁩ في T1
    • الإجابة: نتيجة true: السطر ⁦9⁩ يضيف ⁦25⁩ للشحن

      النتيجة: T1 checkout(100, true)المتوقع ⁦105⁩ والفعلي ⁦105⁩؛ ووحده يشغّل التعليمات الثماني كلهانجاح
    • الإجابة: نتيجة false: يُتخطّى السطر ⁦9⁩ والشحن مجاني

      النتيجة: طلب عضو بقيمة ⁦200.00⁩ فأكثر بعد الخصممسار فقط: أخذ T1 وT2 كل نتيجة فيه من قبلغير لازم لبلوغ ⁦100%⁩
  • الإجابة: نتيجة false: يُتخطّى السطر ⁦4⁩ ويبقى ⁦10%⁩

    سؤال 3: السطر ⁦8⁩: if (total < 200)القرار الثاني: الإجمالي ⁦270⁩ في T2
    • الإجابة: نتيجة true: السطر ⁦9⁩ يضيف ⁦25⁩ للشحن

      النتيجة: طلب غير عضو بأقل من ⁦200.00⁩مسار فقط: أخذ T1 وT2 كل نتيجة فيه من قبلغير لازم لبلوغ ⁦100%⁩
    • الإجابة: نتيجة false: يُتخطّى السطر ⁦9⁩ والشحن مجاني

      النتيجة: T2 checkout(300, false)المتوقع ⁦300⁩ والفعلي ⁦270⁩: خصم ⁦10%⁩ الافتراضيفشل

المساران المتقطعان يجمعان نتائج أخذها T1 وT2 من قبل: إنهما مساران إضافيان لا تتطلبهما تغطية الفروع.

جرّبها: مولّد القيم الحدّية جرّبها: مولّد حالات الاختبار

أخطاء شائعة

  • عدّ النتائج التي لها شيفرة فقط: جانب false من if بلا else نتيجة أيضاً، وهو ما تفوّته تغطية التعليمات.
  • قراءة تغطية التعليمات ⁦100%⁩ كأنها تغطية فروع ⁦100%⁩، أو مقارنة نسب الفروع من أدوات تعدّ الفروع بطرق مختلفة.
  • الوصول إلى النتيجة ثم نسخ النتيجة المتوقعة من الشيفرة بدلاً من المتطلب.
  • افتراض أن تغطية الفروع الكاملة تعني اختبار كل توليفة من النتائج أو كل عدد تكرارات أو كل قيمة بيانات؛ فهي تعني فقط أن كل نتيجة نُفّذت مرة على الأقل.
  • نسيان النتائج الضمنية مثل النتيجة الافتراضية لـswitch، أو افتراض أن حالة القائمة الفارغة مغطاة لأن الخروج من الحلقة نُفّذ.

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

تُقاس تغطية الفروع عادةً على اختبارات الوحدة التي يكتبها المطورون في خط التكامل المستمر (CI). اطلب نسبة الفروع للشيفرة المتغيرة بدلاً من مجموع المشروع، واطلب قائمة النتائج غير المغطاة: فكل منها سؤال عن السلوك. وحين تراجع اختبارات الوحدة الجديدة مع المطور، تحقّق من أن كل اختبار يؤكد النتيجة المأخوذة من المتطلب، لا أنه يصل إلى السطر فحسب.

وفي التقارير سمِّ المعيار والأداة: «شيفرة التسعير، تغطية الفروع (coverage.py مع --branch): ⁦18⁩ من ⁦20⁩ نتيجة؛ والنتيجتان الناقصتان هما غير العضو والسلة الفارغة، وأُضيف لهما اختباران». فالنسبة حدّ أدنى لتصميم الاختبار، وليست دليلاً على الجودة.

تمرين عملي

حقّق تغطية فروع ⁦100%⁩ لإجمالي سلة المشتريات

المطلوب

المتطلب: «إجمالي السلة هو مجموع السعر × الكمية لكل سطر فيها. وتُتجاهل الأسطر التي كميتها ⁦0⁩. وقسيمة الخصم WELCOME20 تخصم ⁦20.00⁩ ريالاً من الإجمالي؛ وبدون قسيمة لا يُخفَّض الإجمالي». وفي الأسفل الدالة cartTotal واختبار الوحدة الوحيد لها، وهو ناجح.

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

المادة
 1  function cartTotal(items, coupon) {
 2    let total = 0;
 3    for (const item of items) {
 4      if (item.qty > 0) {
 5        total = total + item.price * item.qty;
 6      }
 7    }
 8    let discount;
 9    if (coupon === "WELCOME20") {
10      discount = 20;
11    }
12    return total - discount;
13  }

Unit test                                              Expected   Result
T1  cartTotal([{ price: 50, qty: 3 }], "WELCOME20")    130        passed
الخطوات
  1. أوجد كل القرارات، بما فيها الحلقة، وعُدّ نتائجها.
  2. تتبّع T1 واحسب تغطيته للتعليمات وللقرارات.
  3. صمّم اختبارات للنتائج الناقصة، مع نتائج متوقعة من المتطلب.
  4. تتبّع الاختبارات الجديدة: هل تنجح؟ وما الذي لا تتطلبه نسبة ⁦100%⁩ حتى الآن؟
إظهار الإجابة النموذجية
الإجابة النموذجية

القرارات: الحلقة في السطر ⁦3⁩ (الدخول أو الخروج)، وif في السطر ⁦4⁩، وif في السطر ⁦9⁩: ⁦6⁩ نتائج.

يشغّل T1 كل التعليمات: تغطية تعليمات ⁦100%⁩. وبسطر واحد في السلة تدخل الحلقة مرة ثم تخرج، فيأخذ T1 نتيجتَي الحلقة، إضافةً إلى true في السطر ⁦4⁩ وtrue في السطر ⁦9⁩: ⁦4⁩ من ⁦6⁩، أي تغطية قرارات ⁦67%⁩.

الناقص: false في السطر ⁦4⁩ (سطر كميته ⁦0⁩) وfalse في السطر ⁦9⁩ (بلا قسيمة). واختبار واحد يأخذ الاثنتين: T2 cartTotal([{ price: 50, qty: 3 }, { price: 30, qty: 0 }], "") يتوقع ⁦150⁩.

يفشل T2: بدون قسيمة لا تُسنَد أي قيمة إلى discount، فيعيد السطر ⁦12⁩ قيمة 150 - undefined، أي NaN. أبلغ عن المُدخل، والنتيجة المتوقعة ⁦150⁩، والفعلية NaN، والسطر ⁦8⁩؛ والإصلاح هو let discount = 0;. وتغطية التعليمات لا تطلب هذا الاختبار أبداً، لأن نتيجة false في السطر ⁦9⁩ لا تعليمة فيها.

ما زال غير مطلوب: سلة فارغة (فقد خرج T1 من الحلقة)، وقسيمة أكبر من الإجمالي. أضف السلة الفارغة من تحليل القيم الحدّية، واسأل مالك المنتج عن القسيمة الأكبر من الإجمالي.

أسئلة شائعة

هل تغطية الفروع وتغطية القرارات شيء واحد؟

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

هل تعني تغطية الفروع الكاملة أن كل المسارات اختُبرت؟

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

ما نسبة تغطية الفروع التي يجب أن يستهدفها الفريق؟

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

المراجع

  • 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) للقيود الرقمية وقيود الطول.

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

ولّد حالات اختبار منظّمة (المعرّف والعنوان والشروط المسبقة والخطوات والنتيجة المتوقّعة والأولوية) من وصف الميزة وقائمة الحقول.

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

تجميل كود JavaScript وTypeScript بمسافات بادئة وتباعد متسقين.

التنسيق

للتعمّق أكثر

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

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