الاختبار القائم على الخصائص في Rust: فن اكتشاف الأخطاء التي لم تبحث عنها
\n\nهل سبق لك أن كتبت اختبارات وحدة، وقمت بتشغيلها، وكانت جميعها ناجحة، ثم تعطل التطبيق عند أول إدخال غير قياسي من المستخدم؟ مألوف. الاختبارات التقليدية تشبه التحقق من عمل القفل عن طريق إدخال مفتاحك الخاص فقط. ماذا لو أحضر مهاجم (أو مجرد صدفة) مفك براغي، أو مشبك ورق، أو قطعة سلك؟
\n\nفي Rust، اللغة التي تشتهر بسلامتها، توجد أداة قوية لا تسمح فقط بالتحقق من سيناريوهات محددة، بل تثبت أن الكود يتصرف بشكل صحيح لجميع بيانات الإدخال الممكنة. هذا هو الاختبار القائم على الخصائص. واليوم سنقوم بتحليله عمليًا، مع أمثلة حقيقية، حتى تتمكن من تطبيق هذه المعرفة فورًا في مشاريعك.
\n\nما هو الاختبار القائم على الخصائص وكيف يختلف عن الاختبار العادي؟
\n\nتخيل أنك تختبر دالة فرز. يبدو اختبار الوحدة العادي كالتالي:
\n\nfn test_sort() {\n let mut arr = vec![3, 1, 2];\n arr.sort();\n assert_eq!(arr, vec![1, 2, 3]);\n}\n\nهل يعمل؟ يعمل. ولكن ماذا لو كانت المصفوفة مرتبة بالفعل؟ ماذا لو كانت فارغة؟ ماذا لو كانت تحتوي على 10,000 عنصر بترتيب عكسي؟ كتابة اختبارات لكل حالة يدويًا هو طريق لا نهاية له.
\n\nالاختبار القائم على الخصائص يقلب النهج. بدلاً من تحديد بيانات إدخال محددة، تصف خاصية يجب أن تتحقق لأي إدخال صحيح. والمكتبة المتخصصة (مثل proptest أو quickcheck) تقوم بتوليد مئات وآلاف من حالات الاختبار العشوائية، محاولة العثور على تلك الحالة الوحيدة التي تنتهك خاصيتك.
بالنسبة للفرز، يمكن أن تكون الخاصية: "نتيجة الفرز لها دائمًا نفس طول المصفوفة الأصلية، وكل عنصر من المصفوفة الأصلية موجود في النتيجة مرة واحدة بالضبط". أو بشكل أبسط: "النتيجة مرتبة تصاعديًا".
\n\nلماذا Rust هي البيئة المثالية للاختبار القائم على الخصائص
\n\nRust بالفعل تلتقط العديد من الأخطاء في مرحلة الترجمة (إدارة الذاكرة، سباقات البيانات). ولكن هناك فئة من الأخطاء لا يراها المترجم: الأخطاء المنطقية، المعالجة غير الصحيحة للحالات الحدودية، السلوك غير المتوقع مع مجموعات بيانات محددة. هنا يأتي دور الاختبار القائم على الخصائص في Rust.
\n\nإنه يكمل نظام الأنواع بشكل مثالي: تضمن الأنواع أن البيانات لها هيكل صحيح، وتضمن اختبارات الخصائص أن منطق معالجة هذه البيانات صحيح لكامل مساحة القيم الممكنة.
\n\nالأدوات: proptest مقابل quickcheck
\n\nفي نظام Rust البيئي، هناك إطاران رئيسيان: proptest و quickcheck. كلاهما يفعل نفس الشيء، ولكن بفلسفة مختلفة.
\n\n- \n
- quickcheck - أقدم، وأكثر بساطة. يعمل من خلال trait
Arbitrary. تكتب دالة تأخذ بيانات عشوائية، ويتم تشغيل الاختبار عدة مرات. \n - proptest - حديث، وقوي، مع العديد من استراتيجيات توليد البيانات. يسمح بضبط دقيق لنوع البيانات التي يتم توليدها، ويمكنه "تقليص" المثال الفاشل إلى أصغر حالة قابلة للتكرار. هذا هو المعيار الذهبي اليوم. \n
سنستخدم proptest لأنه يعطي تحكمًا أكبر وموثقًا بشكل أفضل. سنقوم بتثبيته:
\n\n# Cargo.toml\n[dev-dependencies]\nproptest = "1.4"\n\nكتابة أول اختبار قائم على الخصائص في Rust
\n\nلنعد إلى الفرز الخاص بنا. لنكتب اختبارًا يتحقق من أن النتيجة مرتبة بالفعل:
\n\nuse proptest::prelude::*;\n\nfn is_sorted<T: Ord>(slice: &[T]) -> bool {\n slice.windows(2).all(|w| w[0] <= w[1])\n}\n\nproptest!