انتقل إلى المحتوى
Topos Uranos

ملخص

البرنامج الذي يُترجَم ويكتب شيئًا معقولًا ليس، لهذا السبب، مختبَرًا. يعلّم هذا الدرس الاختبار بمنهج: استخراج الحالات من المواصفات، بحسب المناطق والحدود؛ وجمعها في برنامج اختبارات آلية يعدّ الإخفاقات ويبلّغ عنها؛ والتحقق بـ assert مما لا يمكن أن يحدث إن كان البرنامج صحيحًا، وبـ static_assert، وقت الترجمة، مما يجب أن تحسبه دالة constexpr؛ وتنفيذ الاختبارات مع الكواشف، التي تكشف السلوك غير المعرَّف الذي تخفيه نتيجة صحيحة. ويقوم كل ذلك على تحذير واحد: يمكن للاختبار أن يُظهر وجود خطأ، ولا يُظهر أبدًا غيابه.

أهداف التعلم

  1. استخراج حالات الاختبار من مواصفات دالة: تقسيمات مدخلاتها، والحدود بينها، والحالات الخاصة، مع النتيجة التي يفرضها الشرط اللاحق في كل منها.
  2. كتابة برنامج اختبارات آلية يقارن النتائج المحصَّلة بالمتوقعة، ويبلّغ عن كل إخفاق دون أن يتوقف، وينتهي بملخص وبرمز خروج يعكسه.
  3. استعمال assert للتحقق من الشروط المسبقة واللامتغيرات، وتفسير سبب اختفائه عند الترجمة بـ -DNDEBUG، وسبب وجوب خلوّ شرطه من الآثار الجانبية.
  4. استعمال static_assert مع دوال constexpr لاختبار النتائج وقت الترجمة، وتنفيذ الاختبارات مع الكواشف لكشف السلوك غير المعرَّف الذي لا تكشفه النتائج.

ما الاختبار

اختبار برنامج هو تنفيذه على مدخلات مختارة ومقارنة ما يفعله بما كان ينبغي أن يفعله. وتتألف كل حالة اختبار من جزأين: مُدخَل يحقق الشرط المسبق، والنتيجة التي يفرضها الشرط اللاحق له، محسوبةً سلفًا وبطريق آخر (يدويًا، أو بصيغة أخرى، أو بجدول معروف). فبلا نتيجة متوقعة لا يوجد اختبار، بل تنفيذ فحسب: أن ننظر إلى المخرجات ونجدها «معقولة» انطباعٌ لا مقارنة.

غير أن دالةً ذات وسيط واحد من النوع int تقبل أكثر من أربعة مليارات مُدخَل، ودالةً ذات وسيطين نحو 1,8⋅10191{,}8 \cdot 10^{19} زوج؛ ولا تستوفيها كلَّها أي مجموعة من الاختبارات. وإذا كان الأمر كذلك، فالاختبارات تفحص جزءًا ضئيلًا من الحالات الممكنة، ولا يلزم من نجاحها جميعًا أن الدالة صحيحة. وقد صاغ ذلك إدسخر ديكسترا سنة 1970: يمكن للاختبارات أن تُظهر وجود الأخطاء، ولا تُظهر أبدًا غيابها. وهذا هو التمييز الذي تقيمه دورة «المنطق الرياضي 1» بين التحقق من حالات والبرهان: التحقق من أن صيغةً صادقة لبعض القيم لا يبرهن أنها صادقة لجميعها، في حين أن البرهان (وهو في هذه الدورة اللامتغير في درس تصميم حلقات صحيحة) يغطي جميع الحالات دفعة واحدة. والاختبارات لا تحل محل ذلك الاستدلال: إنها تجد الأخطاء التي فاتته.

ومن ذلك يلزم معيار اختيار الحالات. فإذا كانت الاختبارات لا تستطيع تأكيد الصحة، فقيمتها في العثور على الأخطاء، وحالة الاختبار الجيدة هي التي يكون احتمال إخفاقها أكبر. والحالة التي اختارها كاتبها لأنه كان يعلم أنها ستنجح لا تُخبر بشيء. ويُظهر ذلك البرنامج الآتي بدالة يجب أن تعيد أكبر عنصر في متجه غير فارغ.

برنامج بلغة C++
#include <print>#include <vector>​// أكبر عنصر في قائمة بيانات.// الشرط المسبق: values ليس فارغًا.// الشرط اللاحق: يعيد أكبر عنصر في values.int largest(const std::vector<int>& values){    int best{0};    for (const int v : values) {        if (v > best) {            best = v;        }    }    return best;}​int main(){    std::println("{}", largest({3, 9, 4}));    std::println("{}", largest({7}));    std::println("{}", largest({1, 2, 3, 4}));    std::println("{}", largest({-5, -2, -8}));}
المخرجات
9740

الحالات الثلاث الأولى هي التي يكتبها أي أحد أولًا، والدالة تنجح فيها: 9 و7 و4 صحيحة. أما الرابعة فتعطي 0، وهي قيمة ليست في المتجه أصلًا، في حين كان ينبغي أن تعطي −2-2. والخطأ في السطر 9: يبدأ best من 0 لا من العنصر الأول، فلا تكون الدالة صحيحة إلا إذا كان أحد العناصر موجبًا أو صفرًا. وكانت الحالات النموذجية تشترك في هذه الخاصية دون أن يختارها أحد؛ أما الرابعة فتخرقها عمدًا. والتصحيح هو int best{values[0]};، الذي يجعله الشرط المسبق مشروعًا، لأنه يضمن وجود values[0].