يا أهلاً وسهلاً فيكم يا جماعة الخير. اسمي أبو عمر، مبرمج فلسطيني قضيت سنين طويلة من عمري بين الأكواد والخوارزميات، وشفت العجب العجاب في عالم البرمجة. اليوم بدي أحكيلكم قصة صارت معي ومع فريقي، قصة علمتنا درس قاسي لكنه مهم جداً عن جودة البرمجيات.
كنا شغالين على نظام مالي حساس جداً، نظام فيه عمليات تحويل ودفع، يعني الغلطة فيه “بخراب بيوت”. الفريق كان شغال ليل نهار، والكل كان همه الأول والأخير هو الجودة. وكأي فريق محترف، اعتمدنا على اختبارات الوحدات (Unit Tests) بشكل كبير. بعد أسابيع من الشغل الشاق، اجتمعنا في يوم من الأيام والفرحة مش سايعتنا، لوحة المراقبة (Dashboard) بتضوي أخضر فاقع: “Test Coverage: 100%”. يا سلام! احتفلنا وشربنا القهوة واحنا حاسين حالنا “ملوك” البرمجة، الكود تبعنا مغطى بالكامل بالاختبارات، ومستحيل يكون فيه أي مشكلة.
لكن أنا، بطبعي “الختيار” الشكاك، كان في إشي جواتي مش مرتاح. إشي بقولي “يا أبو عمر، القصة مش بالبساطة هاي”. دخلت على الكود، على دالة (function) بسيطة لكنها حساسة، كانت بتحسب عمولة معينة. الدالة كانت إشي زي هيك:
// The original correct function
function calculateCommission(amount, rate) {
if (amount <= 0) {
return 0;
}
return amount * rate;
}
رحت بكل بساطة غيرت إشارة الضرب (*) إلى قسمة (/). يعني عملت تخريب متعمد وبسيط. المفروض أي اختبار محترم “يصرخ” ويحكيلي إنه في مصيبة صارت. شغّلت كل الاختبارات مرة ثانية وأنا بترقب النتيجة… والمفاجأة؟ كل الاختبارات نجحت!
هون يا جماعة الخير، أدركنا إنه تغطيتنا للاختبارات كانت مجرد رقم أخضر على الشاشة، والثقة الحقيقية في الكود كانت “صفر على الشمال”. اختباراتنا كانت بتمر على الكود، لكنها ما كانت بتتأكد من صحة النتيجة. كانت اختبارات “وهمية”. ومن هذه اللحظة المظلمة، بدأت رحلتنا مع البطل الخارق: الاختبار الطفري (Mutation Testing).
ما هي مشكلة تغطية الكود (Code Coverage)؟
قبل ما نغوص في الحل، خلينا نفهم أصل المشكلة. تغطية الكود (Code Coverage) هي مقياس بقيس نسبة الكود المصدري اللي تم تنفيذه أثناء تشغيل الاختبارات. يعني لو عندك 100 سطر كود، واختباراتك مرت على 90 سطر منهم، فالتغطية عندك 90%.
المشكلة إنه هذا المقياس بجاوب على سؤال واحد فقط: “هل تم تنفيذ هذا السطر من الكود؟”. لكنه ما بجاوب على السؤال الأهم: “هل تم اختبار سلوك هذا السطر بشكل صحيح؟”.
تغطية الكود مثل مفتش الجودة اللي بيمشي في كل غرف المصنع (تغطية 100%)، لكنه ما بفحص إذا الآلات بتشتغل صح أو بتنتج المنتج المطلوب. هو بس بتأكد إنه دخل كل الغرف. وهذا بالضبط ما كان يحدث معنا، اختباراتنا كانت “تمشي” على الكود، لكنها لم تكن “تختبره” فعليًا.
باختصار، تغطية الكود العالية تمنعك من وجود كود غير مختبَر، لكنها لا تضمن أن الكود المختبَر يعمل بشكل صحيح.
الاختبار الطفري (Mutation Testing): البطل الخارق الذي لم نكن نعلم أننا بحاجة إليه
وهنا يأتي دور “الاختبار الطفري”. تخيل معي إنه في “عفريت” صغير أو “مُحوّر” (Mutant) بدخل على الكود تبعك وببدأ يعمل تغييرات صغيرة وخبيثة. مثلاً:
- بغير إشارة الجمع (+) إلى طرح (-).
- بغير شرط “أكبر من” (>) إلى “أكبر من أو يساوي” (>=).
- بحذف سطر كامل من الكود.
مهمة اختباراتك القوية هي اكتشاف هذا العفريت و”قتله”. كيف بتقتله؟ بأنها تفشل. إذا فشل اختبار واحد على الأقل بعد التغيير، بنحكي إنه “المُحوّر قُتل” (Mutant was killed). وهذا شيء ممتاز، لأنه يعني إنه اختباراتك حساسة للتغييرات وقوية.
أما المصيبة، فهي إذا “نجا المُحوّر” (Mutant survived). هذا يعني إن العفريت غير الكود، ورغم هيك كل اختباراتك نجحت! وهذا دليل قاطع على وجود ثغرة في اختباراتك. أنت لا تختبر هذا الجزء من المنطق البرمجي بشكل صحيح.
دورة حياة “المُحوّر” (Mutant)
العملية بتتم بشكل آلي عبر أدوات متخصصة، وبتمر بالخطوات التالية:
- توليد المُحوّرات: الأداة بتقرأ الكود الأصلي وبتنشئ منه نسخ كثيرة، كل نسخة فيها تغيير صغير واحد (طفرة).
- تشغيل الاختبارات: لكل نسخة “مُحوّرة”، الأداة بتشغل كل مجموعة الاختبارات عليها.
- تحليل النتائج:
- مُحوّر مقتول (Killed): اختبار واحد على الأقل فشل. (ممتاز ✅)
- مُحوّر ناجٍ (Survived): كل الاختبارات نجحت. (سيء جداً ❌)
- مهلة زمنية (Timeout): التغيير سبب حلقة لا نهائية أو بطء شديد. يعتبر “مقتول” غالباً.
- حساب النتيجة: في النهاية، الأداة بتعطيك “معدل الطفرات” (Mutation Score)، وهو نسبة المحوّرات المقتولة إلى إجمالي المحوّرات. هذا الرقم هو مؤشر حقيقي لجودة اختباراتك.
Mutation Score = (Killed Mutants / Total Mutants) * 100
مثال عملي: من تغطية 100% إلى كشف الحقيقة المرة
خلينا نرجع لمثالنا الأول ونشوف كيف الاختبار الطفري كشف المستور. سنستخدم لغة JavaScript للتوضيح.
الكود الأصلي ودالة الاختبار الوهمية
هذه هي الدالة التي نريد اختبارها:
// file: calculator.js
function calculateCommission(amount, rate) {
if (amount <= 0 || rate < 0) {
return 0;
}
return amount * rate;
}
وهذا هو الاختبار “الوهمي” الذي أعطانا تغطية 100% لكنه لا قيمة له:
// file: calculator.test.js
test('commission calculator runs without crashing', () => {
// هذا الاختبار يتأكد فقط أن الدالة أعادت "شيئاً ما"
// لا يهم ما هو هذا الشيء، لذلك هو اختبار ضعيف جداً
expect(calculateCommission(100, 0.05)).toBeDefined();
});
هذا الاختبار يمر على كل أسطر الدالة (عندما تكون المدخلات صالحة)، وبالتالي نحصل على تغطية 100% للجزء الرئيسي من المنطق.
تشغيل أداة الاختبار الطفري
الآن، سنستخدم أداة مثل Stryker (لـ JavaScript/TypeScript) لتحليل هذا الكود. عندما تعمل الأداة، ستقوم بتوليد “مُحوّرات” مثل:
- المُحوّر الأول: تغيير
amount * rateإلىamount / rate. - المُحوّر الثاني: تغيير
amount <= 0إلىamount < 0. - المُحوّر الثالث: حذف السطر
return amount * rate;.
عندما تشغل Stryker، ستحصل على تقرير مفصل، والجزء الأهم فيه هو قائمة “المُحوّرات الناجية”:
----------------|---------------|--------------------------
File | % score | # killed | # survived
----------------|---------------|--------------------------
calculator.js | 33.33 | 1 | 2
----------------|---------------|--------------------------
Survived mutants:
1. ArithmeticOperator @ line 5:8
- return amount * rate;
+ return amount / rate;
Ran 1 test (0 killed)
2. ConditionalExpression @ line 2:5
- if (amount <= 0 || rate < 0)
+ if (amount < 0 || rate < 0)
Ran 1 test (0 killed)
النتيجة كارثية! معدل الطفرات 33% فقط، وهناك “مُحوّران” نجيا. هذا يخبرنا بالضبط أين تكمن المشكلة: اختبارنا لم يتحقق من صحة العملية الحسابية، ولم يختبر حالة الحافة (edge case) عندما يكون المبلغ مساوياً للصفر.
إصلاح الاختبارات لـ “قتل” المُحوّرات
الآن، مسلحين بهذه المعرفة، يمكننا كتابة اختبارات أفضل بكثير:
// file: calculator.test.js (The new, improved version)
// هذا الاختبار "يقتل" المُحوّر الحسابي
test('should calculate the correct commission', () => {
// نتوقع نتيجة دقيقة، وليس فقط "أي شيء"
expect(calculateCommission(100, 0.05)).toBe(5);
});
// هذا الاختبار "يقتل" مُحوّر الشرط
test('should return 0 for zero amount', () => {
expect(calculateCommission(0, 0.05)).toBe(0);
});
test('should return 0 for negative rate', () => {
expect(calculateCommission(100, -0.05)).toBe(0);
});
الآن لو أعدنا تشغيل أداة الاختبار الطفري، ستفشل الاختبارات عند تغيير * إلى / لأن 100 / 0.05 لا تساوي 5. وستفشل أيضاً عند تغيير <= إلى < لأن حالة الصفر لم تعد مشمولة. والنتيجة؟
----------------|---------------|--------------------------
File | % score | # killed | # survived
----------------|---------------|--------------------------
calculator.js | 100.00 | 3 | 0
----------------|---------------|--------------------------
الآن فقط، يمكننا أن نثق في اختباراتنا. الآن أصبح الرقم الأخضر يعني شيئًا حقيقيًا.
نصائح من خبرة أبو عمر
بعد ما استخدمنا هاي التقنية في مشاريع كثيرة، جمعتلكم شوية نصائح من القلب:
-
ابدأ صغيراً وبالتدريج
الاختبار الطفري عملية بطيئة جداً لأنها بتشغل كل اختباراتك مئات أو آلاف المرات. لا تحاول تشغيلها على كل المشروع من أول يوم. ابدأ بوحدة (module) حرجة أو جديدة، وحسّن جودة اختباراتها، ثم انتقل لغيرها.
-
لا تسعَ لنسبة 100%
مثل تغطية الكود، الوصول إلى معدل طفرات 100% قد يكون مكلفًا وغير عملي. بعض المُحوّرات تكون “مكافئة” (Equivalent Mutants)، أي أنها لا تغير سلوك البرنامج (مثلاً تغيير
i++إلى++iفي سياق معين). ركز على الوصول لنسبة عالية (مثلاً فوق 85%) في الأجزاء الحساسة من نظامك. -
ادمجه في خط أنابيب التكامل المستمر (CI/CD) بحكمة
بسبب بطئه، قد لا يكون من العملي تشغيله مع كل `commit`. استراتيجية جيدة هي تشغيله عند إنشاء طلب دمج (Pull Request) على الفرع الرئيسي، أو تشغيله بشكل دوري كل ليلة والحصول على تقرير في الصباح.
-
استخدم “المُحوّرات الناجية” كقائمة مهام
كل “مُحوّر ناجٍ” هو بمثابة تذكرة (ticket) مجانية ومفصلة تخبرك: “هناك ضعف في اختباراتك في هذا المكان بالضبط”. تعامل مع هذه القائمة كفرصة تعليمية لتحسين جودة الكود والاختبارات.
-
أشهر الأدوات حسب اللغة
لكل لغة أدواتها، وهذه بعض الأمثلة لتبدأ بها:
- JavaScript/TypeScript: Stryker
- Java/JVM: PITest
- Python: mutmut
- .NET: Stryker.NET
الخلاصة: من الأرقام الخادعة إلى الثقة الحقيقية 🎯
في عالمنا الرقمي اليوم، لم تعد كتابة الكود الذي “يعمل” كافية. يجب أن نكتب كودًا يمكننا الوثوق به، كودًا صلبًا ومتينًا في وجه التغييرات والأخطاء غير المتوقعة. تغطية الاختبارات (Code Coverage) هي خطوة أولى جيدة، لكنها ليست نهاية الطريق.
الاختبار الطفري (Mutation Testing) هو الخطوة التالية، هو المجهر الذي يكشف لنا نقاط الضعف الحقيقية في شبكة أماننا (اختباراتنا). إنه يحول تركيزنا من “هل اختبرنا الكود؟” إلى “ما مدى جودة اختباراتنا؟”.
قد تكون العملية أبطأ وأكثر تكلفة في البداية، لكن الثقة التي تكتسبها في نظامك لا تقدر بثمن. ففي المرة القادمة التي ترى فيها نسبة تغطية 100%، اسأل نفسك: هل هذا رقم حقيقي أم مجرد وهم؟
تذكروا دايماً، الكود اللي ما بنقدر نكسره بالاختبارات، هو الكود اللي بنقدر نوثق فيه. والله ولي التوفيق. 😉