ماذا لو كانت اختبارات الوحدة الخاصة بك كاذبة؟
\nلقد كتبت مئة اختبار وحدة. إنها ناجحة. يبدو كود C++ مثالياً. ولكن عند تشغيل التطبيق في بيئة الإنتاج، يتعطل مع خطأ تجزئة (segmentation fault) في الطلب رقم 100500. هل هذا الألم مألوف؟
\nاختبارات الوحدة التقليدية تتحقق فقط مما توصلت إليه أنت بنفسك. إنها عمياء تجاه السيناريوهات التي لم تخطر ببالك. الاختبار القائم على الخصائص (أو property-based testing، PBT) يحل هذه المشكلة بشكل جذري: فهو لا يتحقق من أمثلة محددة، بل يثبت خصائص (properties) الكود الخاص بك لـ ملايين من المدخلات العشوائية.
\nفي هذه المقالة، سنشرح كيفية تطبيق الاختبار القائم على الخصائص في C++ باستخدام مكتبة RapidCheck، وسنرى كيف يكتشف الأخطاء التي تفوتها اختبارات الوحدة العادية. هل أنت مستعد لجعل الكود الخاص بك يعاني حقاً؟ هيا بنا.
\n\nالاختبار القائم على الخصائص: الفلسفة في 30 ثانية
\nتخيل أنك تختبر دالة فرز. اختبار عادي: sort([3,1,2]) == [1,2,3]. حسناً، تحققنا من حالة واحدة.
نهج الاختبار القائم على الخصائص: "لأي قائمة من الأعداد الصحيحة، بعد الفرز لا يتغير الطول، العناصر مرتبة تصاعدياً، وكل عنصر من القائمة الأصلية موجود في القائمة المرتبة". يقوم الإطار بتوليد مئات القوائم العشوائية (بما في ذلك الفارغة، والمكررة، والتي تحتوي على أعداد سالبة) ويتحقق من هذه الخصائص.
\nالفكرة الأساسية: أنت تصف الثوابت (properties) التي يجب أن تتحقق دائماً، وليس مدخلات ومخرجات محددة.
\n\nمتى يكون PBT مفيداً بشكل خاص في C++؟
\n- \n
- الخوارزميات وهياكل البيانات (الفرز، البحث، الرسوم البيانية) \n
- المحللات (Parsers) وأدوات التسلسل (Serializers) (التشفير/فك التشفير) \n
- الدوال الرياضية (العمليات العكسية، نطاقات القيم) \n
- التعامل مع الذاكرة (تسريبات، تحرير مزدوج) \n
- أي كود يحتوي على ثوابت واضحة \n
RapidCheck: صديقك الجديد الأفضل
\nتوجد عدة مكتبات لـ PBT في C++، لكن RapidCheck هي الأكثر نضجاً وسهولة في الاستخدام. إنها مستوحاة من مكتبة QuickCheck الخاصة بـ Haskell وتتكامل بشكل ممتاز مع Google Test و Catch2.
\nالتثبيت بسيط: إنها مكتبة header-only، يكفي نسخ الملفات من include/rapidcheck إلى مشروعك. أو استخدم Conan/vcpkg.
الاختبار الأول: خاصية العملية العكسية
\nلنفترض أن لدينا دالة تقوم بتشفير سلسلة نصية إلى base64 وفك تشفيرها مرة أخرى. ما الخاصية الواضحة؟ "فك التشفير بعد التشفير يعيد السلسلة النصية الأصلية".
\n#include <rapidcheck.h>\n#include <string>\n\nstd::string encode_base64(const std::string& input);\nstd::string decode_base64(const std::string& input);\n\nint main() {\n rc::check("فك التشفير بعد التشفير يعيد النص الأصلي",\n [](const std::string& original) {\n // مولد السلسلة النصية مدمج بالفعل\n auto encoded = encode_base64(original);\n auto decoded = decode_base64(encoded);\n RC_ASSERT(decoded == original);\n });\n return 0;\n}\n\nماذا يحدث هنا؟ يقوم RapidCheck بتوليد سلاسل نصية عشوائية (بما في ذلك الفارغة، والتي تحتوي على Unicode، والبايتات الصفرية). لكل منها، يتحقق من الخاصية. إذا وجد مثالاً مضاداً، فإنه يقوم بتصغيره (shrinking) إلى أصغر مثال لا يزال يكسر الاختبار. هذا ذهب: أنت لا تحصل فقط على "يوجد خطأ"، بل على حالة محددة قابلة للتكرار.
\n\nخصائص متقدمة: أكثر من مجرد مساواة
\nالرتابة (Monotonicity) والثبات (Idempotence)
\nبالنسبة لدالة remove_duplicates:
- \n
- الثبات (Idempotence): إذا تم تطبيقها مرتين، لا تتغير النتيجة. \n
- الترتيب غير مهم: القائمة الناتجة تحتوي على نفس عناصر القائمة الأصلية، ولكن بدون تكرار. \n
#include <vector>\n#include <algo