تغطية الفروع والقرارات
تقيس تغطية القرارات (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وكل حلقة، في سطر مستقل، ثم ابحث داخل التعابير عن?:.
تغطية الفروع وعلاقتها بتغطية القرارات
يدرّس منهج 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 أو أكثر). واختبار واحد يأخذ الاثنتين: T2checkout(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% noneif (member)القرار الأول: نتيجتانالإجابة: نتيجة
true: السطر 4 يجعل الخصم 20%سؤال 2: السطر 8:if (total < 200)القرار الثاني: الإجمالي 80 في T1الإجابة: نتيجة
true: السطر 9 يضيف 25 للشحنالنتيجة: T1checkout(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 والشحن مجانيالنتيجة: T2checkout(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- أوجد كل القرارات، بما فيها الحلقة، وعُدّ نتائجها.
- تتبّع T1 واحسب تغطيته للتعليمات وللقرارات.
- صمّم اختبارات للنتائج الناقصة، مع نتائج متوقعة من المتطلب.
- تتبّع الاختبارات الجديدة: هل تنجح؟ وما الذي لا تتطلبه نسبة 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.0المنهج التأسيسي لاختبار البرمجيات: مستويات الاختبار وأنواعه وتقنيات تصميم الاختبارات وإدارة الاختبار والأدوات.
- ISTQB Glossaryالتعريفات القياسية لمصطلحات الاختبار — حالة الاختبار واختبار الانحدار والخطورة والأولوية وتحليل القيم الحدّية وغيرها.
- NIST SP 500-235: Structured Testing: A Testing Methodology Using the Cyclomatic Complexity Metricالاختبار المهيكل (اختبار المسارات الأساسية): استخدام مقياس التعقيد الدوري لـ McCabe لاشتقاق مجموعة أساسية من المسارات المستقلة لاختبارها — وهو أشمل من تغطية التعليمات والفروع.
تدرّب بأدوات متقن
ولّد القيم الحدّية (min-1 وmin وmin+1 والقيمة الاسمية وmax-1 وmax وmax+1) للقيود الرقمية وقيود الطول.
ولّد حالات اختبار منظّمة (المعرّف والعنوان والشروط المسبقة والخطوات والنتيجة المتوقّعة والأولوية) من وصف الميزة وقائمة الحقول.
تجميل كود JavaScript وTypeScript بمسافات بادئة وتباعد متسقين.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.