يا أهلاً وسهلاً فيكم يا جماعة، معكم أخوكم أبو عمر.
اسمحولي أبدأ معكم بقصة صارت معي ومع فريقي قبل كم سنة، قصة علّمتنا درس قاسي لكنه مهم جدًا في عالم البرمجة. كنا وقتها شغالين على نظام مالي حساس، وبذلنا فيه مجهود جبار. وقبل إطلاق الميزة الأهم في النظام، قررنا نكون “محترفين” ونطبق كل الممارسات الصحيحة. كتبنا اختبارات الوحدات (Unit Tests) لكل شاردة وواردة، ووصلنا للرقم السحري اللي كل مدير وكل مطور بيحلم فيه: 100% نسبة تغطية للاختبارات (Test Coverage).
بتذكر منيح يومها، كان شعور بالانتصار. طبعنا التقرير وعلقناه على الحيط، وكأنه شهادة جودة. كان الفريق في قمة ثقته بنفسه، وكنا نحكي لبعض: “خلص، الكود هيك صار حديد، فش إشي رح يمرق”. لكن يا فرحة ما تمت…
بعد إطلاق الميزة لمجموعة مستخدمين صغيرة، بلشت توصلنا تقارير عن مشاكل وأخطاء غريبة. أرقام ما بتنحسب صح، حالات طرفية (edge cases) بتسبب انهيار جزئي، وسلوك غير متوقع بالمرة. كنا في حالة صدمة. كيف بيصير هيك وعندنا تغطية 100%؟ فتحنا الكود، وفتحنا الاختبارات… وهون كانت الصدمة الأكبر. اكتشفنا إن اختباراتنا كانت، بلغة أهلنا في فلسطين، “شغل طق حنك”. كانت الاختبارات بتنفذ الكود، لكنها ما كانت تتأكد من صحة النتائج بشكل دقيق. كانت مجرد “تمرين” للكود بدون فحص حقيقي.
هون أدركنا إننا وقعنا في فخ “المقاييس الخادعة”. مقياس الـ 100% أعطانا شعور زائف بالأمان، بينما كان الكود تبعنا مثل البيت اللي أساساته هشة. ومن رحم هذه الأزمة، بدأت رحلتنا مع مفهوم غيّر طريقة تفكيرنا في الجودة تمامًا: اختبارات الطفرات (Mutation Testing).
ما هي مشكلة “تغطية الاختبارات” كمقياس وحيد؟
قبل ما نغوص في عالم الطفرات، خلينا نفهم ليش مقياس تغطية الاختبارات لوحده ممكن يكون مضلل. ببساطة، تغطية الاختبارات بتقيس “كم” من الكود تم تنفيذه أثناء الاختبارات، لكنها ما بتقيس “جودة” هذه الاختبارات.
تخيل عندك هاي الدالة البسيطة بالجافاسكريبت اللي بتحدد إذا كان الرقم موجب:
// isPositive.js
function isPositive(num) {
if (num > 0) {
return true;
}
return false;
}
والآن، شوف هذا الاختبار “السيء”:
// isPositive.test.js
test('should run isPositive function', () => {
isPositive(5); // استدعينا الدالة فقط!
});
إذا شغّلت أداة قياس التغطية، رح تعطيك نتيجة 100% لهذه الدالة. ليش؟ لأن الاختبار مر على كل الأسطر البرمجية. لكن هل هذا الاختبار مفيد؟ طبعًا لأ. هو ما بتأكد من أي نتيجة (No Assertion). لو غيرنا الدالة الأصلية لترجع `false` دائمًا، هذا الاختبار رح يضل ينجح!
هذا هو بالضبط “وهم الـ 100%”. اختباراتك موجودة على الورق، لكنها لا تحميك في الواقع.
أدخلوا البطل: اختبارات الطفرات (Mutation Testing)
هنا يأتي دور اختبارات الطفرات، أو كما أحب أن أسميها “اختبارات اختباراتك”. الفكرة عبقرية وبسيطة في جوهرها: إذا كانت اختباراتك جيدة حقًا، فيجب أن تفشل عند إجراء أي تغيير بسيط وهام في الكود الأصلي.
ما هي اختبارات الطفرات؟ شرح بسيط
تخيل أن كودك هو بطل خارق. أداة اختبار الطفرات هي الشرير اللي بيحاول يصنع نسخ “مُحوّرة” (Mutants) من هذا البطل مع تغيير بسيط في حمضه النووي.
- التحوير (Mutation): الأداة تأخذ الكود الأصلي وتُجري عليه تغييرًا صغيرًا جدًا. هذا التغيير يسمى “طفرة” أو “تحوّر”. النسخة الجديدة من الكود تسمى “المتحوّل” (Mutant).
- أمثلة على الطفرات:
- تغيير `>` إلى `>=` أو `<`.
- تغيير `+` إلى `-`.
- حذف استدعاء دالة.
- تغيير `true` إلى `false`.
- التحدي (The Challenge): بعد إنشاء “المتحوّل”، تقوم الأداة بتشغيل كل اختباراتك عليه.
- النتائج المحتملة:
- المتحوّل قُتل (Mutant Killed): هذا هو المطلوب! أحد اختباراتك فشل، وهذا يعني أن اختباراتك قوية بما يكفي لاكتشاف هذا التغيير. هذا نجاح للاختبارات.
- المتحوّل نجا (Mutant Survived): كارثة صغيرة! كل اختباراتك نجحت بالرغم من وجود تغيير في الكود. هذا يعني أن اختباراتك فيها ثغرة، ولا تغطي هذه الحالة. هذا هو المكان الذي تحتاج فيه لتحسين اختباراتك.
النتيجة النهائية هي “คะแนน الطفرة” (Mutation Score)، وهي نسبة “المتحولين المقتولين” إلى إجمالي المتحولين. كلما ارتفعت هذه النسبة، زادت ثقتك في جودة اختباراتك.
مثال عملي يوضح الفكرة
نرجع لدالتنا `isAdult` اللي بتفحص العمر:
// age.js
function isAdult(age) {
return age >= 18;
}
لنفترض أن لدينا هذا الاختبار المبدئي، وهو جيد لكنه ليس مثاليًا:
// age.test.js
test('should return true for age 25', () => {
expect(isAdult(25)).toBe(true);
});
الآن، سنشغل أداة اختبار الطفرات (مثل Stryker Mutator). ستقوم الأداة بإنشاء “متحوّل” تلقائيًا. أحد أشهر الطفرات هو تغيير المعاملات الشرطية.
المتحوّل رقم 1:
function isAdult(age) {
// تم تغيير '>=' إلى '>'
return age > 18;
}
الآن، الأداة ستشغل اختبارنا `isAdult(25)` على هذا الكود المتحوّل. النتيجة ستكون `true`، والاختبار سينجح. ماذا يعني هذا؟
المتحوّل نجا (Mutant Survived)!
اختباراتنا لم تكن دقيقة بما يكفي للقبض على هذا التغيير. المشكلة أننا لم نختبر الحالة الطرفية (edge case) وهي عمر 18 بالضبط.
كيف نقتل المتحوّل؟
نضيف اختبارًا جديدًا للحالة الطرفية:
// age.test.js (النسخة المحسّنة)
test('should return true for age 25', () => {
expect(isAdult(25)).toBe(true);
});
// الاختبار الجديد الذي سيقتل المتحوّل
test('should return true for exact age 18', () => {
expect(isAdult(18)).toBe(true);
});
الآن، عندما يتم تشغيل هذا الاختبار الجديد على الكود المتحوّل (`age > 18`)، فإن `isAdult(18)` ستُرجع `false`، بينما الاختبار يتوقع `true`. وبالتالي، سيفشل الاختبار.
المتحوّل قُتل (Mutant Killed)! 🎉
لقد قمنا بتقوية مجموعة اختباراتنا. الآن نحن واثقون أكثر من أن دالتنا تعمل بشكل صحيح حول الرقم 18.
كيف بدأنا رحلتنا مع اختبارات الطفرات؟
بعد اكتشافنا للمشكلة، كان القرار حاسمًا. بدأنا بدمج اختبارات الطفرات في عملنا. كانت الخطوات الأولى مؤلمة بعض الشيء لكنها كشفت الكثير.
الخطوات الأولى
- ابدأ صغيرًا: لم نحاول تشغيلها على كامل المشروع دفعة واحدة، فهذا سيستغرق وقتًا طويلاً وستكون النتائج محبطة. اخترنا وحدة (module) واحدة حساسة وبدأنا بها.
- شغّل الأداة: استخدمنا أداة Stryker Mutator لأن مشروعنا كان بلغة TypeScript.
- تقبّل الصدمة: كانت نسبة تغطية الاختبارات 100%، لكن “คะแนน الطفرة” الأولي كان 45% فقط! هذا الرقم كان الدليل القاطع على ضعف اختباراتنا. أكثر من نصف التغييرات المحتملة على الكود لم تكن اختباراتنا قادرة على كشفها.
- لا تستهدف 100%: تمامًا مثل تغطية الاختبارات، الحصول على “คะแนน طفرة” 100% أمر صعب ومكلف وقد لا يكون عمليًا. استهدف نسبة عالية ومعقولة (مثل 80-85%) على الأجزاء الحساسة من الكود.
- ادمجه في الـ CI/CD بحذر: اختبارات الطفرات بطيئة جدًا لأنها تعيد تشغيل اختباراتك مئات أو آلاف المرات. لا تشغلها على كل `commit`. الاستراتيجية الأفضل هي تشغيلها على طلبات الدمج (Pull Requests) وفقط على الملفات التي تغيرت، أو تشغيلها بشكل دوري (ليلاً مثلاً) على المشروع كاملاً.
- استخدمه كأداة تعلّم: هي أفضل أداة وجدتها لتعليم المطورين الجدد (وحتى القدامى) كيفية كتابة اختبارات ذات معنى. عندما يرون بأعينهم كيف “ينجو” متحول بسبب اختبارهم الضعيف، يتعلمون الدرس أسرع من أي محاضرة نظرية.
- بعض الطفرات غبية، تجاهلها: أحيانًا، تقوم الأداة بإنشاء طفرات لا تغير سلوك الكود (Equivalent Mutants) أو طفرات في أماكن غير مهمة مثل جمل الطباعة (Logging). معظم الأدوات الحديثة تسمح لك بتجاهل هذه الطفرات لتحسين التجربة.
ol>
تحليل النتائج وتحسين الاختبارات
أجمل ما في أدوات اختبار الطفرات هو تقاريرها المفصلة. تريك بالضبط أي “متحوّل” نجا، وما هو التغيير الذي حدث في الكود. بدأنا بالمرور على “الناجين” واحدًا تلو الآخر، وفي كل مرة كنا نسأل أنفسنا:
“لماذا لم يكتشف اختبارنا هذا التغيير؟ ما هي الحالة التي أهملناها؟”
ثم كنا نكتب الاختبار الذي “يقتل” هذا المتحوّل. كانت عملية تكرارية، لكن مع كل “متحوّل مقتول”، كانت ثقتنا في الكود تزداد بشكل حقيقي، وليس بشكل وهمي.
نصائح من خبرة أبو عمر
بعد سنوات من استخدام هذه التقنية، اسمحوا لي أن أشارككم بعض النصائح العملية:
الخلاصة: ما بعد وهم المقاييس 🎯
خُلاصة الحكي يا جماعة، إن مقياس تغطية الاختبارات مهم، ولكنه ليس إلا نقطة البداية. هو يخبرك بأنك “مررت” من هذا الشارع، لكنه لا يخبرك إن كنت قد نظرت يمينًا ويسارًا. اختبارات الطفرات هي التي تختبر قوة ملاحظتك.
رحلتنا من الاحتفال بنسبة 100% وهمية إلى المعاناة مع نسبة 45% حقيقية، ثم العمل على رفعها تدريجيًا، غيرت ثقافتنا في الفريق. لم نعد نسأل “هل كتبنا اختبارات؟” بل أصبحنا نسأل “هل اختباراتنا قوية بما يكفي؟”.
نصيحتي الأخيرة لك: لا تثق ثقة عمياء في الأرقام. تحدَّ مقاييسك دائمًا. جرب إحدى أدوات اختبار الطفرات على جزء صغير من مشروعك. النتائج قد تكون صادمة، لكنها ستكون بداية الطريق نحو بناء برمجيات تثق بها حقًا، وتنام بعدها قرير العين.
جربوها، وادعولي. بالتوفيق!