الاختبار العشوائي (Fuzzing) والاختبار القائم على الخصائص: اكتشاف الأخطاء التي لم تتخيلها حالات اختبارك

الاختبار العشوائي (Fuzzing) والاختبار القائم على الخصائص: اكتشاف الأخطاء التي لم تتخيلها حالات اختبارك

12

November 2026 Thursday

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

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

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

حدود الاختبار القائم على الأمثلة

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

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

وتكملها التقنيات التوليدية. فبدلًا من أن تسأل هل يعمل الكود مع هذه الأمثلة المحددة، تسأل هل يحقق الكود قواعد معينة لأي مدخل، ثم تبحث بإصرار عن مثال مضاد.

شرح الاختبار القائم على الخصائص

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

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

وتضيف الأطر الجيدة ميزة حاسمة تسمى التقليص (Shrinking). فالمدخل الفاشل المولَّد عشوائيًا يكون عادةً كبيرًا ومشوشًا، مثل قائمة من 87 عنصرًا بقيم غامضة. يبسّطه الإطار تلقائيًا بحذف العناصر وتقليل القيم ما دام الفشل مستمرًا، حتى يصل إلى مثال مضاد أدنى، ربما قائمة من عنصرين. وهذا يحوّل فشلًا غامضًا إلى حالة واضحة قابلة للتشخيص.

توجد تنفيذات حديثة لكل منظومة تقريبًا: Hypothesis لبايثون، وfast-check لجافاسكريبت وتايبسكريبت، وjqwik لجافا، وScalaCheck لسكالا، وFsCheck لـ.NET، وproptest لـ Rust، وrapid لـ Go. وتتشارك جميعها المفاهيم نفسها: المولِّدات التي تنتج المدخلات، والخصائص التي تعبّر عن القواعد، والمقلِّصات التي تصغّر الحالات الفاشلة.

العثور على خصائص جيدة

أصعب جزء في الاختبار القائم على الخصائص ليس الأداة بل التفكير في الخصائص. وتساعد عدة أنماط متكررة.

خصائص الذهاب والعودة تنص على أن الترميز ثم فك الترميز يعيد القيمة الأصلية. فتحويل كائن إلى JSON ثم تحليله يجب أن يعطي كائنًا مساويًا. وضغط البيانات ثم فكها يجب أن يكون بلا فقدان. وهذه من أسهل الخصائص وأكثرها إنتاجية.

الثوابت تصف حقائق يجب أن تبقى صحيحة بعد أي عملية. تبقى الشجرة المتوازنة متوازنة بعد الإدراج. ولا يصبح رصيد الحساب سالبًا بعد سحب صالح. ويساوي عدد العناصر في سلة التسوق مجموع الكميات.

تكافؤ الأثر عند التكرار يعني أن تطبيق العملية مرتين له الأثر نفسه كتطبيقها مرة، مثل توحيد صيغة البريد الإلكتروني.

خصائص المرجع تقارن التنفيذ بمرجع موثوق. فيمكن اختبار خوارزمية محسّنة جديدة مقابل نسخة بطيئة لكنها صحيحة بوضوح، أو وحدة أُعيدت كتابتها مقابل القديمة أثناء الترحيل.

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

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

كتابة مولّدات جيدة

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

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

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

ما هو الاختبار العشوائي (Fuzzing)

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

المُشوِّش (Fuzzer) برنامج يولّد مدخلات بشكل متكرر ويشغّل الهدف ويراقب إشارات الفشل مثل الانهيارات وانتهاكات التأكيدات وأخطاء الذاكرة وانتهاء المهلة وتقارير المعقِّمات. وبخلاف الاختبار القائم على الخصائص الذي يستهدف غالبًا الصحة المنطقية بمدخلات منظمة، يستهدف الـFuzzing تقليديًا المتانة والأمن بمدخلات مشوهة عن قصد.

وهناك ثلاثة أساليب عامة. الـFuzzing الأعمى أو العشوائي يرسل بايتات اعتباطية ويأمل الأفضل. والـFuzzing القائم على القواعد أو التوليد يبني المدخلات من مواصفات الصيغة، مثل بنية طلب HTTP أو عبارة SQL، فيصل إلى كود أعمق لأن المدخلات تجتاز التحقق الأولي. والـFuzzing القائم على التحوير يبدأ من مدخلات صالحة نموذجية، تسمى مجموعة البذور، ويجري تغييرات عشوائية صغيرة مثل قلب البتات وحذف أجزاء ودمج قطع.

الـFuzzing الموجَّه بالتغطية

الاختراق الذي جعل الـFuzzing أكثر فاعلية بكثير هو التوجيه بالتغطية. فأدوات مثل AFL التي أنشأها ميشال زاليفسكي، وخلفاؤها AFL++ وlibFuzzer وHonggfuzz، تزوّد البرنامج الهدف بأدوات قياس لمراقبة مسارات الكود التي يمر بها كل مدخل. وعندما يصل مدخل متحور إلى كود جديد يُحفظ في المجموعة ويصبح أساسًا لمزيد من التحويرات. ومع الوقت يطوّر المُشوِّش مدخلات تتعمق أكثر في البرنامج، وتجد مسارات لم يكن إنسان ليبنيها.

تحوّل حلقة التغذية الراجعة هذه الـFuzzing إلى بحث موجَّه، وسجله مثير للإعجاب. فخدمة OSS-Fuzz من غوغل تشوّش مئات المشاريع مفتوحة المصدر باستمرار وأبلغت عن عشرات الآلاف من الأخطاء، كثير منها ثغرات أمنية. ومن العيوب المشهورة المكتشفة بالـFuzzing ثغرات في مكتبات الصور وتنفيذات TLS ومحركات المتصفحات.

ويُدمج الـFuzzing عادةً مع المعقِّمات (Sanitizers)، وهي ميزات مترجم تكتشف المشكلات وقت التشغيل. يجد AddressSanitizer فيض المخازن والاستخدام بعد التحرير، ويلتقط UndefinedBehaviorSanitizer العمليات غير المعرّفة كفيض الأعداد الموقعة، ويجد MemorySanitizer قراءة الذاكرة غير المهيأة. وبدون المعقِّمات لا تسبب كثير من أخطاء إفساد الذاكرة انهيارات ظاهرة، ولن يلاحظها المُشوِّش.

الـFuzzing خارج الكود الأصلي

رغم ارتباط الـFuzzing بلغتي C وC++، فهو مفيد في اللغات الآمنة للذاكرة أيضًا. والأهداف مختلفة هناك: الذعر والاستثناءات غير المعالجة والحلقات اللانهائية والاستهلاك المفرط للذاكرة والأخطاء المنطقية. لدى Go دعم Fuzzing مدمج في حزمة الاختبار القياسية، ولدى Rust أداة cargo-fuzz، ولدى جافا Jazzer، ولدى بايثون Atheris، ولدى جافاسكريبت Jazzer.js. وتشويش محلل JSON أو محمّل إعدادات أو معالج رفع ملفات في أي منها قد يكشف حالات حجب الخدمة والعيوب المنطقية.

ويمكن تشويش خدمات الويب وواجهات البرمجة على مستوى الشبكة. فأدوات مثل RESTler وSchemathesis وffuf تولّد طلبات بناءً على وصف OpenAPI، وتغيّر المعاملات، وتبحث عن أخطاء الخادم وانتهاكات المخطط والسلوك غير المتوقع. وبما أن Schemathesis تستخدم مخطط الواجهة لقيادة اختبار قائم على الخصائص، فإنها تجسر بين التقنيتين بأناقة: الخاصية هي ألا يعيد الخادم خطأ 500 أبدًا وأن يستجيب دائمًا وفق مخططه الموثق.

موضع هذه التقنيات في خط التسليم

الاختبار التوليدي ليس بديلًا عن اختبارات الوحدة والتكامل والاختبارات الشاملة. إنه طبقة مكمّلة بملف تكلفة خاص.

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

الـFuzzing أكثر استهلاكًا للموارد ويستفيد من التشغيل لساعات أو أيام. يمكن تشغيل جلسات قصيرة من بضع دقائق في التكامل المستمر لكشف الانحدارات باستخدام المجموعة المحفوظة. أما الحملات الأطول فتعمل باستمرار على أجهزة مخصصة أو عبر خدمات مثل OSS-Fuzz وClusterFuzzLite. وينبغي تصغير الانهيار الذي يجده المُشوِّش وتحويله إلى حالة اختبار حتمية وإيداعها مع الإصلاح.

وينبغي تطبيق التقنيتين بانتقائية حيث تقدمان أكبر قيمة: المحللات والمسلسِلات، ومعالجات البروتوكولات، ومنطق المصادقة والتفويض، والحسابات المالية والتسعيرية، وتحويلات البيانات، وآلات الحالات، وكل ما يعالج مدخلات غير موثوقة. هذه هي المجالات التي تكثر فيها المدخلات غير المتوقعة وتكلّف فيها الأعطال أكثر.

مزالق شائعة

تتكرر أخطاء عدة. كتابة خصائص تحصيل حاصل، كالتأكيد بأن الدالة تعيد ما تعيده الدالة، تمنح ثقة زائفة. وإعادة تنفيذ الدالة داخل الاختبار قريب من هذا الخطأ. ينبغي أن تعبّر الخصائص عن قواعد مستقلة لا أن تكرر المنطق.

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

ومن المزالق تجاهل المولّدات البطيئة أو غير المستقرة، مما يدفع الفرق إلى تعطيل الاختبارات. أبقِ المولّدات فعالة والاختبارات سريعة.

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

مثال تطبيقي: دالة تسعير

لنفترض أن منصة تجزئة لديها دالة تطبق خصمًا نسبيًا على سعر بالسنتات ثم تضيف الضريبة. يكتب المطور ثلاثة اختبارات بالأمثلة: خصم 10 بالمئة على 1000 سنت يعطي 900؛ وخصم 0 بالمئة يترك السعر كما هو؛ وخصم 100 بالمئة يعطي صفرًا. تنجح كلها.

ثم يضيف الفريق اختبارات قائمة على الخصائص باستخدام Hypothesis، ويكتبون مولّدات للسعر (أعداد صحيحة من 0 إلى 10 ملايين سنت) ونسبة الخصم (من 0 إلى 100) ومعدل الضريبة (كسور عشرية من 0 إلى 0.30). ويعرّفون ثلاث خصائص. الأولى: السعر النهائي لا يكون سالبًا أبدًا. الثانية: الخصم الأكبر لا يعطي سعرًا نهائيًا أعلى عند ثبات السعر ومعدل الضريبة. الثالثة: تطبيق خصم صفر وضريبة صفر يعيد السعر الأصلي تمامًا.

عند أول تشغيل تفشل الخاصية الثانية. يبلّغ Hypothesis عن مثال مضاد مقلَّص: السعر 1 ومعدل الضريبة 0.25 والخصم 0 يعطي 1 سنت، لكن الخصم 1 يعطي 2 سنت. ارتفع السعر النهائي حين زاد الخصم. والسبب هو التقريب: تقرّب الدالة المبلغ المخصوم ومبلغ الضريبة كلًّا على حدة بالتقريب نحو الأعلى عند المنتصف، فللأسعار الصغيرة جدًا تتقرب الضريبة لأعلى أكثر مما يوفره الخصم. ولم يكن اختبار يدوي ليجرّب سعر سنت واحد، بينما قد يرى العملاء في كتالوج به منتجات ترويجية بأسعار بضع سنتات مجاميع غير متسقة.

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

بلغ الجهد الإجمالي نحو ساعتين، بما فيها تعلّم المكتبة. والنتيجة: عيبان حقيقيان اكتُشفا قبل الإصدار، ودالة أمتن، ومجموعة اختبارات تواصل استكشاف مدخلات جديدة كلما شُغّلت.

البدء في فريقك

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

الخاتمة

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

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