تغطية الشروط والمسارات

تنظر تغطية الشروط (Condition Coverage) إلى داخل القرار. فالقرار account.active && (amount <= account.dailyLimit || otpOk) يضم ثلاثة شروط ذرّية (Atomic conditions)، أي تعابير منطقية لا تحتوي على && أو ||. تسأل تغطية الفروع هل كان القرار ككل صحيحاً وخاطئاً، وتسأل تغطية الشروط هل كان كل شرط كذلك. أما تغطية المسارات (Path Coverage) فتنظر إلى الدالة كلها: هل نُفّذ كل طريق من نقطة الدخول إلى نقطة الخروج؟

يتجاوز المعياران تغطية التعليمات وتغطية الفروع من الدرسين السابقين، ولا يصلح أيٌّ منهما هدفاً بنسبة ⁦100%⁩ في كل مكان. فالمهارة هي اختيار المجموعة الجزئية المناسبة للمنطق عالي المخاطر والتصريح بما اخترته.

لماذا يهم

يكفي اختباران لتغطية فروع القرار كاملةً مهما كان عدد شروطه. لنفترض أن المطور نسي الأقواس: account.active && amount <= account.dailyLimit || otpOk. ولأن أسبقية && أعلى من أسبقية ||، صار الحساب الموقوف الذي يؤكد رمز التحقق لمرة واحدة (OTP) يحصل على الموافقة على تحويله. وإذا كان اختبارا تغطية الفروع هما «كل شيء صحيح» و«كل شيء خاطئ»، أعطت النسختان النتائج نفسها ومرّ العيب.

العوامل المنطقية الخاطئة والأقواس الناقصة والشروط التي لا أثر لها عيوب نموذجية في القرارات المركّبة، وكثيراً ما تقع في القواعد التي تقرر المال والصلاحيات. والمعايير على مستوى الشروط تختار تركيبات قليلة تفحص أثر كل شرط في القرار، وهناك تظهر هذه العيوب، دون اختبار كل التركيبات.

الشروط والقرارات وتغطية الشروط

كما في الدرس السابق، القرار نقطة تختار فيها الشيفرة بين نتائج، مثل جملة if أو شرط حلقة. والشروط هي التعابير المنطقية داخله؛ والشرط الذرّي في مسرد ISTQB هو الشرط الذي لا يحتوي على أي عامل منطقي. والدالة أدناه فيها قرار واحد بثلاثة شروط ذرّية: A هو account.active، وB هو amount <= account.dailyLimit، وC هو otpOk.

تقيس تغطية الشروط نتائج الشروط التي نفّذتها الاختبارات: الصحيح والخاطئ لكل شرط ذرّي. وهي لا تتضمن تغطية الفروع. فاختباران، حساب نشط تجاوز حدّه بلا رمز تحقق (A صحيح، وB خاطئ، وC خاطئ) وحساب موقوف ضمن حدّه مع رمز تحقق (A خاطئ، وB صحيح، وC صحيح)، يعطيان تغطية شروط بنسبة ⁦100%⁩، ومع ذلك يُرفض التحويلان ولا يُنفَّذ APPROVED أبداً.

  • تغطية الشروط والقرارات (Condition/Decision Coverage، وتُسمّى أيضاً Decision Condition Coverage) تسدّ الثغرة: فهي تشترط كل نتيجة لكل شرط وكل نتيجة لكل قرار.
  • التقييم المختصر (Short-circuit evaluation، ويسمّيه مسرد ISTQB باسم Short-circuiting): في JavaScript وJava وC# يتوقف && و|| بمجرد معرفة النتيجة (وكذلك and وor في Python)، فإذا كان A خاطئاً لا يُقيَّم B ولا C أصلاً. ويصمّم هذا الدرس الاختبارات على جدول الحقيقة، لكن اختبار الحساب الموقوف أعلاه لا يُقيّم B ولا C عند التشغيل، فالأداة التي تقيس الشروط لن تحتسب لأيٍّ منهما النتيجة «صحيح». والأدوات تختلف أيضاً: بعضها يعدّ كل طرف فرعاً، وبعضها لا يقيس إلا القرارات الكاملة أو الأسطر؛ فافحص أداتك قبل الإبلاغ عن تغطية الشروط.
 1  function reviewTransfer(account: Account, amount: number, otpOk: boolean) {
 2    if (account.active && (amount <= account.dailyLimit || otpOk)) {
 3      return 'APPROVED';
 4    }
 5    return 'REJECTED';
 6  }

مثال تطبيقي: من تغطية الشروط إلى MC/DC

المتطلب: «يُوافَق على التحويل ضمن الحد اليومي إذا كان الحساب نشطاً. وفوق الحد يحتاج إلى تأكيد رمز التحقق OTP. وتُرفض دائماً التحويلات من الحسابات الموقوفة». ثلاثة شروط تعطي ⁦2 × 2 × 2 = 8⁩ تركيبات: اكتبها في جدول حقيقة (Truth table) كما في درس قواعد العمل، مع القرار المتوقع من المتطلب.

تشترط تغطية الشروط والقرارات المعدّلة (Modified Condition/Decision Coverage, MC/DC) إثبات أن كل شرط يغيّر القرار وحده. والطريقة المعتادة زوج استقلال (Independence pair): اختباران لا يختلفان إلا في ذلك الشرط ويعطيان قرارين مختلفين. ولإيجاد زوج B ثبّت A وC، واقلب قيمة B، وابحث عن تغيّر في القرار: حين يكون A صحيحاً وC خاطئاً يعطي الصفان ⁦2⁩ و⁦4⁩ صحيحاً ثم خاطئاً. ويشترط معيار الطيران DO-178C تغطية MC/DC لأكثر مستويات البرمجيات حرجاً فيه، وهو المستوى A.

جدول الحقيقة للقرار A && (B || C): الصفان ⁦4⁩ و⁦5⁩ يعطيان تغطية شروط كاملة والقرار خاطئ فيهما، والصفان ⁦1⁩ و⁦8⁩ يعطيان تغطية الشروط والقرارات، ومجموعة MC/DC المختارة هي ⁦2⁩ و⁦3⁩ و⁦4⁩ و⁦7⁩، وتغطية الشروط المتعددة تحتاج إلى الثمانية.
الصف · A B Cالقرارتغطية الشروطاختبارانتغطية الشروط والقراراتاختبارانMC/DC⁦4⁩ اختباراتتغطية الشروط المتعددة⁦8⁩ اختبارات
1T T TTليس ضمن هذه المجموعةاختبارليس ضمن هذه المجموعةاختبار
2T T FTليس ضمن هذه المجموعةليس ضمن هذه المجموعةيُثبت B (مع ⁦4⁩)اختبار
3T F TTليس ضمن هذه المجموعةليس ضمن هذه المجموعةيُثبت A (مع ⁦7⁩) وC (مع ⁦4⁩)اختبار
4T F FFاختبارليس ضمن هذه المجموعةيُثبت B (مع ⁦2⁩) وC (مع ⁦3⁩)اختبار
5F T TFاختبارليس ضمن هذه المجموعةليس ضمن هذه المجموعةاختبار
6F T FFليس ضمن هذه المجموعةليس ضمن هذه المجموعةليس ضمن هذه المجموعةاختبار
7F F TFليس ضمن هذه المجموعةليس ضمن هذه المجموعةيُثبت A (مع ⁦3⁩)اختبار
8F F FFليس ضمن هذه المجموعةاختبارليس ضمن هذه المجموعةاختبار
  • تركيبة يختبرها هذا المعيار
  • الصف ⁦7⁩، حساب موقوف تجاوز حدّه مع رمز التحقق: الاختبار الذي يكشف الأقواس الناقصة

الشرط A هو account.active، وB هو amount <= account.dailyLimit، وC هو otpOk. والرمز T يعني صحيحاً وF خاطئاً، والقرار T يعني الموافقة على التحويل.

  • تغطية الشروط: الصفان ⁦4⁩ و⁦5⁩، والقرار خاطئ فيهما. تغطية الشروط والقرارات: الصفان ⁦1⁩ و⁦8⁩.
  • MC/DC: الصفوف ⁦2⁩ و⁦3⁩ و⁦4⁩ و⁦7⁩. يُثبَت أثر B بالصفين ⁦2⁩ و⁦4⁩، وC بالصفين ⁦3⁩ و⁦4⁩، وA بالصفين ⁦3⁩ و⁦7⁩. أي أربعة اختبارات لثلاثة شروط: فتغطية MC/DC تحتاج دائماً إلى اختبار واحد على الأقل زيادةً على عدد الشروط.
  • يمكن أن يستخدم A الصفين ⁦2⁩ و⁦6⁩ أيضاً. اختر ⁦3⁩ و⁦7⁩: فالصف ⁦7⁩، حساب موقوف تجاوز حدّه مع رمز التحقق، يكشف عيب الأقواس الناقصة. وحين يترك لك المعيار خياراً، اختر بحسب المخاطر.
  • تغطية الشروط المتعددة (Multiple Condition Coverage): الصفوف الثمانية كلها. وتتضاعف مع كل شرط؛ فعشرة شروط تعني ⁦1,024⁩ تركيبة.
النسخة النصية
Decision: A && (B || C)
A = account.active    B = amount <= account.dailyLimit    C = otpOk

Row   A  B  C   Decision   MC/DC independence pairs
 1    T  T  T   T
 2    T  T  F   T          B: rows 2 and 4
 3    T  F  T   T          C: rows 3 and 4,  A: rows 3 and 7
 4    T  F  F   F          B: rows 2 and 4,  C: rows 3 and 4
 5    F  T  T   F
 6    F  T  F   F
 7    F  F  T   F          A: rows 3 and 7
 8    F  F  F   F

Condition coverage            rows 4, 5          decision never T
Condition/decision coverage   rows 1, 8
MC/DC                         rows 2, 3, 4, 7
Multiple condition coverage   all 8 rows

جرّبها: مولّد جداول القرار

تغطية المسارات، ولماذا ينفجر عددها مع الحلقات

المسار (Path) طريق واحد عبر الشيفرة من نقطة الدخول إلى نقطة الخروج، وتسأل تغطية المسارات هل نفّذت الاختبارات كل طريق. والمسارات تتضاعف: ثلاث جمل if متتالية لها ⁦8⁩ مسارات، في حين يكفي اختباران لتغطية كل فروعها.

والحلقات أسوأ بكثير، لأن كل عدد من التكرارات مسار مختلف. ففي cartTotal يسلك كل عنصر أحد طريقين، فللسلة التي تضم n من العناصر 2^n من المسارات: ⁦1,048,576⁩ مساراً لسلة ممتلئة بعشرين عنصراً، و⁦2,097,151⁩ مساراً إجمالاً. أما الحلقة التي لا حدّ أعلى لتكراراتها فعدد مساراتها لا نهائي. وبعض المسارات غير قابل للتنفيذ (Infeasible) أيضاً: شروطها متناقضة فلا يستطيع أي مُدخل تنفيذها.

  • في الحلقات طبّق القيم الحدّية على عدد التكرارات: صفر، وواحد، وعدد معتاد، والحد الأقصى (هنا ⁦0⁩ و⁦1⁩ و⁦5⁩ و⁦20⁩ عنصراً)، وأضف ⁦21⁩ عند إضافة العناصر، وهو ما يجب أن ترفضه السلة.
  • يكشف التفكير في المسارات عيوباً تحتاج إلى تسلسل من الطرق، مثل حالة متبقية من تكرار تُفسد التكرار التالي.
 1  function cartTotal(items: Item[]): number {   // a cart holds 0 to 20 items
 2    let total = 0;
 3    for (const item of items) {
 4      if (item.onSale) {
 5        total = total + item.salePrice;
 6      } else {
 7        total = total + item.price;
 8      }
 9    }
10    return total;
11  }

Items in cart     Paths
0                 1
1                 2
2                 4
3                 8
20                1,048,576
0 to 20           2,097,151 in total

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

المسارات الأساسية والتعقيد الدوري

اختبار المسارات الأساسية (Basis Path Testing)، وهو جوهر منهج الاختبار المهيكل (Structured testing) في NIST SP ⁦500-235⁩، يُبقي المسارات قابلة للإدارة. والتعقيد الدوري (Cyclomatic Complexity)، ويُكتب v(G)، هو أكبر عدد من المسارات المستقلة خطياً عبر مخطط تدفق التحكم (Control flow graph) للدالة: v(G) = edges − nodes + 2، أي عدد الحواف (Edges)، وهي فروع المخطط، ناقص عدد العُقد (Nodes) زائد ⁦2⁩. وحين تكون كل القرارات ثنائية الاتجاه يساوي ذلك عدد القرارات زائد واحد. والمجموعة الأساسية (Basis set) هي v(G) من المسارات يمكن تركيب أي مسار آخر منها.

في cartTotal قراران، الحلقة وجملة if، لذا v(G) = 3، وتكوّن ثلاثة اختبارات مجموعة أساسية: سلة فارغة، وعنصر عادي واحد، وعنصر مخفّض واحد. والمجموعة الأساسية تضمن تغطية الفروع كاملةً لا كل تركيبة: إنها حدّ أدنى منهجي، لا تغطية كاملة للمسارات.

  • بعدّ جملة if وحدها تكون v(G) = 2 في reviewTransfer. أما NIST SP ⁦500-235⁩ فيعدّ أيضاً كل && و|| ذي تقييم مختصر قراراً، لأن كلاً منهما قد يتخطى بقية التعبير، فتصبح ⁦4⁩؛ وكذلك تعدّهما القاعدة complexity في ESLint. وقد تختلف الأدوات، فلا تقارن إلا أرقام الأداة نفسها.
  • اعتبر ارتفاع v(G) إشارة: فالدالة تحتاج إلى اختبارات كثيرة ويصعب فهمها، فهي تستحق مراجعة أدق أو إعادة هيكلة.

أخطاء شائعة

  • قراءة تغطية شروط بنسبة ⁦100%⁩ على أنها «كل قرار سار في الاتجاهين».
  • اختبار قرار مركّب مثل a && (b || c) مرة صحيحاً ومرة خاطئاً فقط، فيمرّ عامل منطقي خاطئ أو أقواس ناقصة.
  • افتراض أن اختباراً نفّذ شرطاً تخطّاه التقييم المختصر.
  • استهداف تغطية مسارات بنسبة ⁦100%⁩ في شيفرة فيها حلقات بدلاً من المسارات الأساسية وأعداد تكرار مختارة.
  • الإبلاغ عن التركيبات المستحيلة أو المسارات غير القابلة للتنفيذ على أنها ثغرات بدلاً من تسجيلها مع السبب.
  • اختيار أزواج MC/DC آلياً بدلاً من الزوج الذي يختبر التركيبة الأعلى خطراً.

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

استخدم المعايير على مستوى الشروط للمنطق عالي المخاطر مثل المدفوعات والصلاحيات والأهلية. وفي المراجعات اطلب من المطور أن يشرح لك القرارات المركّبة. ضع جدول الحقيقة في تصميم الاختبار، وعلّم الصفوف المستحيلة مع السبب، وحوّل الصفوف المختارة إلى حالات اختبار مبنية على البيانات (Data-driven) نتائجها المتوقعة من المتطلب.

في التقارير سمِّ المعيار والنطاق: «قاعدة اعتماد التحويل: MC/DC بأربعة اختبارات». ونبّه مبكراً إلى التعقيد الدوري المرتفع أو القرار كثير الشروط: فهو يبيّن عدد الاختبارات التي يحتاجها المنطق، وقد يكون سبباً لتبسيط الشيفرة أولاً.

تمرين عملي

صمّم اختبارات MC/DC لقاعدة العمر

المطلوب

المتطلب: «يمكن لمن بلغ ⁦18⁩ عاماً فأكثر فتح حساب. ومن عمره من ⁦13⁩ إلى ⁦17⁩ عاماً يحتاج إلى موافقة أحد الوالدين. ولا يمكن لمن هو دون ⁦13⁩ عاماً فتح حساب». وفيما يلي دالة المطور واختبارا الوحدة الخاصان بها، وهما يبلغان تغطية فروع بنسبة ⁦100%⁩.

صمّم اختبارات على مستوى الشروط بأعمار فعلية، واكتشف ما فات اختبارَي الوحدة.

المادة
 1  function canOpenAccount(age: number, parentConsent: boolean): boolean {
 2    if (age >= 18 || (parentConsent && age > 13)) {
 3      return true;
 4    }
 5    return false;
 6  }

Unit tests                         Expected   Result
U1  canOpenAccount(25, true)       true       passed
U2  canOpenAccount(9, false)       false      passed
الخطوات
  1. سمِّ الشروط الذرّية في السطر ⁦2⁩ واكتب جدول الحقيقة دون الصفوف المستحيلة.
  2. جِد زوج استقلال لكل شرط، واجمع الأزواج في أصغر مجموعة تحقق MC/DC.
  3. اختر أعماراً عند حدود المتطلب، مع النتائج المتوقعة منه.
  4. تتبّع الاختبارات: أيها يفشل، وما الذي لم يفحصه U1 وU2 قط؟
إظهار الإجابة النموذجية
الإجابة النموذجية

A هو age >= 18، وB هو parentConsent، وC هو age > 13؛ والقرار A || (B && C). ولا يمكن أن يكون A صحيحاً وC خاطئاً (فكل عمر من ⁦18⁩ فأكثر أكبر من ⁦13⁩)، فتبقى ⁦6⁩ تركيبات من ⁦8⁩.

بكتابة كل صف بترتيب A ثم B ثم C: يُثبَت A بالصفين TFT (صحيح) وFFT (خاطئ)، وB بالصفين FTT وFFT، وC بالصفين FTT وFTF. وأصغر مجموعة MC/DC هي TFT وFFT وFTT وFTF: أربعة اختبارات.

الأعمار: ⁦18⁩ دون موافقة ← مسموح؛ ⁦17⁩ دون موافقة ← مرفوض؛ ⁦17⁩ بموافقة ← مسموح؛ ⁦12⁩ بموافقة ← مرفوض. ويضيف تحليل القيم الحدّية ⁦13⁩ بموافقة ← مسموح.

هذا الاختبار يفشل: 13 > 13 خاطئ، فتضع الشيفرة العمر ⁦13⁩ في صف العمر ⁦12⁩ نفسه وترفض حساباً يسمح به المتطلب. أبلغ عن عيب الفارق بواحد في السطر ⁦2⁩؛ والإصلاح هو age >= 13.

بلغ U1 وU2 تغطية فروع بنسبة ⁦100%⁩، لكن العمر وحده حسم النتيجة في U1 ولم تكن في U2 موافقة، فلم تُختبر قاعدة الموافقة قط. كشفت MC/DC التركيبات الناقصة، واختار تحليل القيم الحدّية القيمة التي كشفت العيب.

أسئلة شائعة

هل تتضمن MC/DC تغطية القرارات؟

نعم، في القرارات التي تُطبَّق عليها: فكل زوج استقلال يُظهر القرار مرة صحيحاً ومرة خاطئاً. ولا تقول شيئاً عن الشيفرة خارج هذه القرارات، ولا عن سلوك لم يُنفَّذ أصلاً.

أي معيار أستخدم؟

اجعل تغطية الفروع خط الأساس. وللقرارات المركّبة في القواعد عالية المخاطر أضف تغطية الشروط والقرارات أو MC/DC. وتغطية الشروط المتعددة معقولة التكلفة حين يضم القرار شرطين أو ثلاثة. أما التغطية الكاملة للمسارات فتناسب الدوال الصغيرة الخالية من الحلقات فقط؛ وفيما عداها اختبر مجموعة أساسية وأعداد التكرار المهمة.

هل عليّ حساب التعقيد الدوري يدوياً؟

نادراً. أدوات التحليل الساكن وأدوات الفحص (Linters) تحسبه لكل دالة؛ فالقاعدة complexity في ESLint مثلاً تنبّه إلى كل دالة تتجاوز حداً تضبطه أنت. لكن حسابه يدوياً مرة واحدة لدالة صغيرة يساعدك على فهم ما يعدّه الرقم ولماذا قد تختلف الأدوات.

إذا غطّيت كل مسار، فهل اختُبرت الدالة بالكامل؟

لا. قد ينجح المسار مع بيانات ويفشل مع غيرها، مثل قسمة لا تفشل إلا عندما تكون قيمة ما صفراً، والمتطلب الذي لم يُنفَّذ قط ليس له مسار أصلاً. تقيس تغطية المسارات الطرق عبر الشيفرة، لا صحة النتائج.

المراجع

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

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

أنشئ جداول القرار من الشروط والإجراءات، وولّد جميع تركيبات القواعد، واختزل المستحيل منها.

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

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

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

نفّذ تحليلاً كاملاً للقيم الحدّية (ثنائي القيم وثلاثي القيم) مع فئات التكافؤ والنتائج المتوقّعة.

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

للتعمّق أكثر

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

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