يا جماعة الخير، السلام عليكم ورحمة الله.
اسمحوا لي اليوم أحكي لكم قصة صارت معي قبل كم سنة، قصة علّمتني درس قاسي لكنه ثمين جداً في عالم البرمجة. كنت وقتها شغال على نظام مالي حساس لإحدى الشركات، وكان في جزء من الكود مسؤول عن حساب العمولات والخصومات. كود دقيق، الغلطة فيه بتكلف مصاري، يعني ما في مجال للمزح.
أنا، كعادتي، كتبت الكود وبعدها كتبت له الاختبارات الوحدوية (Unit Tests). شغّلت الاختبارات، والنتيجة؟ 100% تغطية للكود (Code Coverage). لمعت عيوني وشعرت بفخر كبير. قلت في نفسي: “خلص، هي أبو عمر ختمها! الكود مغطى بالكامل، فش ولا سطر كود إلا والاختبار مر عليه. الشغل مية المية!”. عرضت الشغل على مدير المشروع وأنا كلي ثقة.
بعد أسبوع، بيجيني زميلي وبحكيلي: “أبو عمر، في مشكلة غريبة في حساب العمولات للحالات اللي الخصم فيها صفر”. تفاجأت! كيف يعني؟ أنا اختباراتي غطت كل شي! فتحت الكود، وبعد شوية بحث وتحليل، اكتشفت خطأ بسيط جداً في شرط منطقي. كان لازم أستخدم `>=` (أكبر أو يساوي) ولكني استخدمت `>` (أكبر تماماً). هذا الخطأ الصغير كان يعني أن الحالات الحدّية (edge cases) ما بتشتغل صح.
قعدت مع حالي وسألت نفسي: “يا زلمة، كيف هيك صار؟ كيف الاختبارات اللي بتغطي الكود 100% ما مسكت هالغلطة؟”. هنا كانت الصدمة ونقطة التحول. اكتشفت أن اختباراتي كانت “واثقة من نفسها أكثر من اللازم”، كانت سطحية. هي مرّت على الكود، لكنها لم تتحقق من صحة منطقه بشكل كافٍ. ومن هنا، بدأت رحلتي مع مفهوم غيّر طريقة تفكيري في الاختبارات تماماً: الاختبار الطفري (Mutation Testing).
ما هي مشكلة تغطية الكود (Code Coverage)؟
قبل ما نغوص في الاختبار الطفري، خلينا نوضح ليش نسبة تغطية الكود 100% ممكن تكون خدعة. تغطية الكود بتقيس ببساطة “كم سطر من الكود تم تنفيذه” أثناء تشغيل الاختبارات. هي لا تقيس جودة التحققات (Assertions) داخل هذه الاختبارات.
لأبسطها لكم بمثال من الحياة: تخيل أنك حارس أمن ومهمتك تتأكد من أن مبنى مكون من 100 غرفة آمن. لو مررت بكل غرفة وفتحت بابها وأغلقته، تقريرك سيقول أنك “غطيت” 100% من المبنى. لكن ماذا لو لم تتحقق من أن النوافذ مغلقة؟ أو أن نظام الإنذار يعمل؟ أنت غطيت المكان، لكنك لم تضمن جودة الأمان فيه. هذا بالضبط ما تفعله اختبارات التغطية السطحية.
نصيحة من أبو عمر: تغطية الكود هي نقطة بداية ممتازة، لكنها ليست خط النهاية أبداً. هي مؤشر على أن اختباراتك تصل إلى الكود، وليست مؤشراً على أن اختباراتك تتحقق من صحة هذا الكود.
أهلاً بك في عالم الاختبار الطفري (Mutation Testing)
هنا يأتي دور البطل الحقيقي في قصتنا. الاختبار الطفري هو تقنية لا تختبر الكود الخاص بك مباشرة، بل تختبر “اختباراتك”. نعم، قرأتها صح، هي تختبر مدى قوة وصلابة اختباراتك.
ما هو الاختبار الطفري؟ ببساطة يا جماعة
الفكرة عبقرية وبسيطة في نفس الوقت. تقوم أداة الاختبار الطفري بالخطوات التالية:
- أخذ نسخة من الكود الأصلي: الكود الذي يعمل وتمر جميع اختباراتك عليه بنجاح.
- صناعة “الطفرات” (Mutants): تقوم الأداة بإحداث تغييرات صغيرة جداً ومتعمدة في نسختك من الكود لإنشاء نسخ “مُحوّرة” أو “طافرة”. هذه التغييرات تحاكي الأخطاء البسيطة التي يمكن أن يقع فيها المبرمج.
- تغيير `>` إلى `<=`
- تغيير `+` إلى `-`
- حذف استدعاء دالة معينة.
- تغيير `if (condition)` إلى `if (true)`
- تشغيل اختباراتك: يتم تشغيل مجموعة الاختبارات الكاملة الخاصة بك ضد كل “طفرة” على حدة.
- تحليل النتائج:
-
الطفرة “قُتلت” (Killed Mutant): إذا فشل أحد اختباراتك عند تشغيله على الكود المُحوّر، فهذا شيء ممتاز! يعني أن اختبارك قوي بما يكفي لاكتشاف هذا النوع من الأخطاء. نقول أن الطفرة “قُتلت”.
-
الطفرة “نجت” (Survived Mutant): إذا مرت جميع اختباراتك بنجاح على الرغم من وجود الطفرة (الخطأ المتعمد)، فهذه هي المشكلة! هذا يعني أن اختباراتك ليست دقيقة بما يكفي لاكتشاف هذا التغيير. هذه الطفرة “الناجية” تكشف عن ثقب في شبكة أمانك الاختبارية.
-
الهدف النهائي هو “قتل” أكبر عدد ممكن من الطفرات. النسبة المئوية للطفرات المقتولة تسمى “مؤشر الطفرات” (Mutation Score)، وهو مقياس أكثر واقعية لجودة اختباراتك من تغطية الكود.
مثال عملي: لنشاهد الطفرات وهي تعمل
الكلام النظري جميل، لكن دعونا نرى هذا على أرض الواقع. سنستخدم مثال بسيط بلغة Python.
الكود الأصلي والدالة المراد اختبارها
لدينا دالة بسيطة تتأكد إذا كان عمر المستخدم مسموحاً به (بين 18 و 60 عاماً).
# user_validator.py
def is_age_valid(age):
"""Checks if age is between 18 and 60 (inclusive)."""
if age >= 18 and age <= 60:
return True
else:
return False
الاختبار “الجيد” (ولكنه غير كافٍ)
سنكتب الآن مجموعة اختبارات تحقق تغطية 100% لهذه الدالة.
# test_user_validator.py
from user_validator import is_age_valid
def test_valid_age_in_middle():
# Test a standard valid age
assert is_age_valid(35) is True
def test_invalid_age_too_young():
# Test an age that is too young
assert is_age_valid(10) is False
إذا قمنا بتشغيل أداة تغطية الكود (مثل `pytest-cov`) على هذه الاختبارات، سنحصل على نسبة 100%. نشعر بالرضا والثقة، أليس كذلك؟ انتظر قليلاً.
دخول المتحولين (Mutants) إلى الساحة
الآن، سنقوم بتشغيل أداة اختبار طفري (مثل `mutmut` في عالم Python) على الكود. ستقوم الأداة بإنشاء عدة طفرات، دعونا نركز على اثنتين منها:
الطفرة رقم 1: تغيير `>=` إلى `>`
ستقوم الأداة بتغيير الكود ليصبح كالتالي:
# Mutant 1
def is_age_valid(age):
if age > 18 and age <= 60: # <-- الطفرة هنا!
return True
else:
return False
الآن، ستقوم الأداة بتشغيل اختباراتنا الموجودة (`test_valid_age_in_middle` و `test_invalid_age_too_young`) ضد هذه الطفرة. هل سيفشل أي من الاختبارين؟
- `is_age_valid(35)`: ستُرجع `True` لأن 35 أكبر من 18. الاختبار يمر.
- `is_age_valid(10)`: ستُرجع `False` لأن 10 ليست أكبر من 18. الاختبار يمر.
النتيجة: كلا الاختبارين مرا بنجاح! هذا يعني أن الطفرة قد نجت (Survived). لقد كشفت لنا للتو أننا لا نملك أي اختبار يتحقق من الحالة الحدّية لعمر 18 عاماً بالضبط!
الطفرة رقم 2: تغيير `and` إلى `or`
# Mutant 2
def is_age_valid(age):
if age >= 18 or age <= 60: # <-- الطفرة هنا!
return True
else:
return False
هذه طفرة خطيرة، فهي تجعل الشرط صحيحاً دائماً تقريباً (أي رقم هو إما أكبر من 18 أو أصغر من 60). لنرَ كيف ستصمد اختباراتنا:
- `is_age_valid(35)`: ستُرجع `True`. الاختبار يمر.
- `is_age_valid(10)`: ستُرجع `True` (لأن 10 أصغر من 60). لكن اختبارنا يتوقع `False`! أخيراً! سيفشل هذا الاختبار!
النتيجة: بما أن أحد الاختبارات فشل، فإن الطفرة قد قُتلت (Killed). هذا جيد، اختبارنا `test_invalid_age_too_young` كان قوياً بما يكفي لقتل هذه الطفرة.
كيف “نقتل” الطفرات الناجية؟
الآن مهمتنا واضحة: يجب أن نكتب اختباراً جديداً لقتل “الطفرة رقم 1” التي نجت. كيف نفعل ذلك؟ ببساطة، نضيف اختباراً يغطي الحالة الحدية التي كشفتها الطفرة.
# test_user_validator.py (النسخة المحسّنة)
from user_validator import is_age_valid
def test_valid_age_in_middle():
assert is_age_valid(35) is True
def test_invalid_age_too_young():
assert is_age_valid(10) is False
# الاختبار الجديد لقتل الطفرة الناجية
def test_valid_age_on_lower_boundary():
"""This test will kill the '>=' to '>' mutant."""
assert is_age_valid(18) is True
# ويمكننا إضافة اختبار للحالة الحدية العليا أيضاً
def test_valid_age_on_upper_boundary():
assert is_age_valid(60) is True
الآن، لو عادت “الطفرة رقم 1” (`age > 18`) للظهور، فإن اختبار `test_valid_age_on_lower_boundary` سيفشل، لأن `is_age_valid(18)` ستُرجع `False` في الكود المُحوّر، بينما اختبارنا يتوقع `True`. وهكذا نكون قد “قتلنا” الطفرة وحصّنا الكود ضد هذا النوع من الأخطاء.
نصائح أبو عمر الذهبية للتعامل مع الاختبار الطفري
بعد سنوات من استخدام هذه التقنية، إليكم بعض النصائح العملية من القلب:
- ابدأ صغيراً: لا تحاول تطبيق الاختبار الطفري على كامل المشروع دفعة واحدة، خاصة لو كان مشروعاً ضخماً وقديماً. ابدأ بوحدة (module) حرجة أو جديدة. اجعله جزءاً من عملية تطوير الميزات الجديدة.
- لا تسعَ لنسبة 100%: على عكس تغطية الكود، الوصول إلى مؤشر طفرات 100% أمر صعب جداً ومكلف وقد لا يكون عملياً. استهدف نسبة 80-85% كبداية، وركز على إصلاح الطفرات الناجية في الأجزاء الأكثر حساسية من تطبيقك.
- الأتمتة هي المفتاح: ادمج الاختبار الطفري في مسار التكامل المستمر (CI/CD). نصيحتي ألا تجعله يمنع عملية البناء (build) في البداية، فقد يكون بطيئاً. يمكنك تشغيله بشكل دوري (مثلاً كل ليلة) ومراجعة التقارير في الصباح.
- تعلم تجاهل “الطفرات المكافئة”: أحياناً، تقوم الأداة بإنشاء طفرة تغير الكود ولكن لا تغير سلوكه المنطقي (Equivalent Mutant). مثلاً، تغيير `i = i + 1` إلى `i++`. هذه الطفرات ستنجو دائماً لأنه لا يمكن كتابة اختبار يفشل بسببها. تعلم كيفية تمييزها وتجاهلها في أداة الاختبار التي تستخدمها.
- استخدمه كأداة تعلّم: أفضل ميزة في الاختبار الطفري هي أنه يعلمك كيف تفكر كمختبِر أفضل. تقارير الطفرات الناجية هي خارطة كنز للثغرات في تفكيرك ومنطقك. مع الوقت، ستبدأ بكتابة اختبارات أقوى وأكثر دقة من المرة الأولى.
الخلاصة: من تغطية الكود إلى ثقة الكود 💡
رحلتي من مطور يثق بنسبة “تغطية الكود 100%” إلى مطور يسعى “لمؤشر طفرات مرتفع” كانت رحلة نضج فكري. الاختبار الطفري أجبرني على التوقف عن سؤال “هل اختبرتُ هذا السطر؟” والبدء بسؤال “هل اختبرتُ هذا السلوك بشكل صحيح؟ هل يمكن أن ينكسر هذا الشرط بطريقة لم أفكر بها؟”.
إنه ينقلنا من المقياس السطحي (الكمية) إلى المقياس العميق (الجودة). لا يتعلق الأمر بكتابة المزيد من الاختبارات، بل بكتابة اختبارات أذكى وأكثر صلابة.
نصيحتي الأخيرة لكم: لا تثقوا باختباراتكم ثقة عمياء، بل تحدّوها باستمرار. الاختبار الطفري هو المطرقة التي تكشف لكم صلابة درعكم البرمجي أو هشاشته. جربوه، وحتى لو لم تعتمدوه في كل مشاريعكم، فإنه سيغير طريقة تفكيركم في جودة الكود إلى الأبد.
ودمتم سالمين مبدعين.