يا أهلاً وسهلاً فيكم يا جماعة، معكم أخوكم أبو عمر.
خلوني أحكيلكم قصة صارت معي قبل كم سنة، قصة علّمتني درس قاسي لكنه مهم جدًا في عالم البرمجة. كنا في الفريق شغالين على نظام مالي حساس، وكان التركيز على الجودة والدقة فوق كل اعتبار. مدير المشروع، الله يسهل عليه، كان عنده هوس بمقاييس الجودة، وعلى رأسها “تغطية الكود” (Code Coverage).
بعد أسابيع من التعب والجهد، وكتابة مئات الاختبارات، في يوم من الأيام، ظهر الرقم السحري على شاشة الـ CI/CD pipeline: Code Coverage: 100%. يا الله، الفرحة اللي كانت في المكتب وقتها لا توصف. شعرنا إننا بنينا قلعة حصينة، كود لا يُقهر، وصرنا نتباهى بهالرقم قدام كل الفرق الثانية. “شوفوا شغلنا يا جماعة، 100% تغطية!”
بعد كم يوم، وفي عز الاحتفالات، وصلتنا رسالة عاجلة من فريق الدعم. في خطأ كارثي بالنظام! عملية حسابية بسيطة بتعطي نتائج غلط في حالات معينة، وسببت شوية مشاكل مالية للعملاء. لما فتحنا الكود عشان نشوف المصيبة، كانت صدمة. الخطأ كان في سطر واحد، سطر بسيط جدًا، والمفروض إنه الاختبارات مغطياه. فتحنا ملف الاختبارات، وفعلاً، لقينا اختبار بستدعي الدالة (function) اللي فيها المشكلة. لكن المشكلة وين؟ الاختبار كان “أهبل”. كان بستدعي الدالة بس ما بتأكد من صحة النتيجة في كل الحالات. كان مجرد “بمر” على الكود عشان يرفع نسبة التغطية.
وقتها، حسيت بغصة. كل هالتعب، وكل هالاحتفالات، كانت مبنية على وهم. مقياس الـ 100% أعطانا شعور زائف بالأمان، وخلانا عميان عن الثغرات الحقيقية في جودة الكود. من يومها، قررت إني ما أكون “عبد للمقاييس”، وبدأت رحلتي في البحث عن طريقة أعمق وأذكى لقياس جودة اختباراتنا… وهون بدأت قصتي مع “الاختبار الطفري”.
لماذا تغطية الكود 100% هي كذبة جميلة؟
قبل ما نهاجم مقياس تغطية الكود، خلينا نكون منصفين. هو مقياس مفيد، لكن لازم نفهم حدوده. ببساطة، تغطية الكود بتجاوب على سؤال واحد فقط: “ما هي أجزاء الكود التي لم يتم تنفيذها أثناء الاختبارات؟”. هذا مفيد جدًا عشان تعرف وين المناطق المظلمة في الكود تبعك اللي ما وصلتها الاختبارات أبدًا.
المشكلة تبدأ لما يصير الهدف هو الوصول لنسبة 100%، بدل ما يكون الهدف هو كتابة اختبارات قوية. هون المطورين بصيروا يكتبوا اختبارات “ضعيفة” بس عشان يرفعوا النسبة.
مثال على اختبار ضعيف بتغطية 100%
تخيل عنا هاي الدالة البسيطة بلغة JavaScript اللي بتحسب الخصم، بشرط إنه نسبة الخصم ما تكون سالبة.
function calculateDiscount(price, percentage) {
if (percentage < 0) {
// المفروض يرمي خطأ أو يرجع صفر
// لكن بالخطأ، المطور تركها فارغة أو بترجع السعر نفسه
return price;
}
const discountAmount = price * (percentage / 100);
return price - discountAmount;
}
الآن، ممكن نكتب الاختبار التالي:
test('calculates discount for a valid percentage', () => {
expect(calculateDiscount(100, 20)).toBe(80);
});
لو شغلنا أداة تغطية الكود على هذا الاختبار، رح تحكيلك إنه التغطية 100% على مسار التنفيذ اللي جربته. لكن ماذا عن الشرط if (percentage < 0)؟ هو ما تم اختباره فعليًا. ممكن مطور يضيف اختبار ثاني عشان “يلمس” هذا السطر:
test('handles negative percentage', () => {
calculateDiscount(100, -10); // لا يوجد تأكيد (assertion)!
});
الآن، أداة تغطية الكود رح تعطيك 100% بكل فخر! كل الأسطر تم “لمسها”. لكن هل اختباراتنا قوية؟ طبعًا لا. لو مطور بالخطأ غيّر المنطق داخل الشرط، الاختبار الثاني ما رح يفشل لأنه ما بتأكد من أي نتيجة.
هذا هو بالضبط جحيم الاختبارات عديمة الفائدة اللي عشنا فيه.
المنقذ: الاختبار الطفري (Mutation Testing)
الاختبار الطفري هو تقنية بتقلب المعادلة. بدل ما تسأل “هل الكود تبعي مغطى؟”، بتسأل سؤال أقوى بكثير: “هل اختباراتي قوية كفاية لتكتشف الأخطاء لو حصلت؟”
الفكرة عبقرية وبسيطة، مثل فكرة اللقاح. عشان تتأكد إنه جهاز المناعة عندك قوي، بنعطيك نسخة مضعّفة من الفيروس. نفس المبدأ هنا:
- الخطوة الأولى: بنشغل كل اختباراتك على الكود الأصلي. المفروض كلها تنجح (لون أخضر).
- الخطوة الثانية (التحوير أو الطفرة): أداة الاختبار الطفري بتيجي وبتاخد نسخة من الكود تبعك، وبتعمل فيه تغييرات صغيرة جدًا وبشكل تلقائي. هاي التغييرات اسمها “طفرات” أو “Mutants”.
- بتغير
a + bإلىa - b. - بتغير
x >= yإلىx > yأوx < y. - بتغير
if (condition)إلىif (true). - بتحذف سطر كود.
- بتغير
- الخطوة الثالثة: لكل “طفرة” (نسخة محوّرة من الكود)، الأداة بترجع بتشغل كل اختباراتك مرة ثانية.
- الخطوة الرابعة (التحليل):
- إذا فشل أحد الاختبارات: ممتاز! هذا اسمه “طفرة قُتلت” (Mutant Killed). هذا يعني إنه اختباراتك قوية كفاية ولاحظت إنه الكود تغير وصار فيه خطأ. ✅
- إذا نجحت كل الاختبارات: مصيبة! هذا اسمه “طفرة نجت” (Mutant Survived). هذا معناه إنه الكود تغير وصار فيه خطأ محتمل، لكن اختباراتك “العمياء” ما لاحظت أي شي. هاي هي الثغرة الحقيقية في جودة اختباراتك. ❌
الهدف هو “قتل” أكبر عدد ممكن من الطفرات. كل طفرة “تنجو” هي بمثابة إشارة ضوئية بتخبرك: “يا مبرمج، اختبارك في هاي المنطقة ضعيف، روح قوّيه”.
لنعد لمثالنا السابق
لنتخيل دالة أبسط للوضوح:
// isAdult.js
function isAdult(age) {
return age >= 18;
}
والاختبار “الضعيف” الذي يعطي تغطية 100%:
// isAdult.test.js
test('should identify an adult', () => {
expect(isAdult(20)).toBe(true);
});
الآن، أداة الاختبار الطفري (مثل Stryker في عالم JavaScript) رح تبدأ شغلها. من الطفرات اللي ممكن تعملها:
- الطفرة 1: تغيير
age >= 18إلىage > 18. - الطفرة 2: تغيير
age >= 18إلىage < 18. - الطفرة 3: تغيير
return age >= 18إلىreturn false.
لنحلل ما سيحدث:
- مع الطفرة 2 و 3: الاختبار
isAdult(20)سيفشل، لأن النتيجة ستكونfalseبدلtrue. إذن، هاتان الطفرتان سيتم “قتلهما”. ممتاز. - مع الطفرة 1 (
age > 18): عندما يتم تشغيل الاختبارisAdult(20)، النتيجة ستكون20 > 18وهيtrue. الاختبار يتوقعtrue، إذن الاختبار سينجح! هذه الطفرة “نجت” (Survived).
التقرير سيخبرك أن هناك طفرة نجت عند مقارنة >=. هذا يصرخ في وجهك: “أنت لم تختبر حالة الحافة (edge case)!”.
لإصلاح هذا، نضيف اختبارًا أقوى يقتل هذه الطفرة:
test('should identify a person who is exactly 18 as an adult', () => {
expect(isAdult(18)).toBe(true);
});
الآن، عندما يتم إنشاء الطفرة age > 18، هذا الاختبار الجديد سيفشل، لأن 18 > 18 هي false، بينما الاختبار يتوقع true. وهكذا نكون قد “قتلنا” الطفرة وحسّنا من جودة اختباراتنا بشكل حقيقي.
نصائح عملية من خبرة أبو عمر
الاختبار الطفري أداة جبارة، لكنها ممكن تكون بطيئة ومكلفة حسابيًا (لأنها بتعيد تشغيل الاختبارات مئات المرات). لذلك، استخدمها بذكاء:
- لا تستبدل، بل أضف: لا تتخلص من تقارير تغطية الكود. استخدمها كخطوة أولى لتحديد الأجزاء غير المختبرة على الإطلاق. ثم استخدم الاختبار الطفري على الأجزاء “المغطاة” لتتأكد من جودة التغطية.
- ابدأ بالمناطق الحرجة: ابدأ بتطبيق الاختبار الطفري على أهم أجزاء نظامك، مثل منطق الحسابات المالية، صلاحيات المستخدمين، أو أي كود أساسي يعتمد عليه باقي التطبيق.
- ادمجه في الـ CI/CD بحذر: تشغيل الاختبار الطفري على كل `commit` قد يكون بطيئًا جدًا. يمكنك تشغيله ليلًا، أو فقط على طلبات السحب (Pull Requests) التي تعدل ملفات مهمة، أو عندما تصل نسبة تغطية الكود التقليدية لرقم معين.
- لا تقدس نسبة الـ 100%: تمامًا مثل تغطية الكود، الوصول لـ “Mutation Score” بنسبة 100% قد يكون صعبًا ومكلفًا. الهدف ليس الرقم بحد ذاته، بل استخدام تقرير “الطفرات الناجية” كدليل إرشادي لتحسين اختباراتك بشكل مستمر. نسبة 80-90% تعتبر ممتازة في معظم المشاريع.
- افهم الطفرات الناجية: لا تصلح الأمور بشكل أعمى. عندما تنجو طفرة، افهم لماذا. هل هو نقص في اختباراتك؟ أم أن الطفرة تنتج كودًا مكافئًا (equivalent code)؟ بعض الأدوات تسمح لك بتجاهل هذه الطفرات.
نصيحة من القلب: “اكتب اختباراتك عشان تفشل، مش عشان تنجح”. غيّر عقليتك من “كيف أجعل هذا الاختبار ينجح؟” إلى “كيف يمكن أن أكسر هذا الكود؟ وما هو الاختبار الذي سيكشف هذا الكسر؟”. هذه العقلية هي جوهر الهندسة البرمجية عالية الجودة.
الخلاصة: من عبودية المقاييس إلى ثقة حقيقية
رحلتنا من وهم الـ 100% إلى حقيقة الاختبار الطفري كانت نقطة تحول في فريقنا. تعلمنا أن الجودة ليست رقمًا يظهر على لوحة تحكم، بل هي ثقة نكتسبها في كل سطر كود نكتبه. الاختبار الطفري لم يكن مجرد أداة، بل كان معلمًا قاسيًا لكنه عادل، كشف لنا ضعفنا وأعطانا الأدوات لنصبح أقوى.
يا جماعة، لا تكونوا عبيدًا للمقاييس. افهموا ما الذي تقيسونه ولماذا. شغلوا عقولكم قبل تشغيل الـ CI/CD pipeline. تغطية الكود تخبرك أين كنت، لكن الاختبار الطفري يخبرك إلى أي مدى يمكنك الوثوق بالطريق الذي سلكته.
ابدأ اليوم، اختر جزءًا صغيرًا وحرجًا في مشروعك، وجرب عليه أداة اختبار طفري. أضمن لك أنك ستتفاجأ بما ستكتشفه. 😉