عندما تنجح الاختبارات ولكن الكود لا يزال "مكسورًا"
\n\nتخيل الموقف: لقد كتبت اختبارات وحدة لمشروع C#، dotnet test يعطي ضوءًا أخضر، وتغطية الكود (code coverage) تبلغ 85%. الفريق راضٍ، وCI/CD يعمل بشكل مثالي. ولكن بعد شهر، يظهر خطأ في بيئة الإنتاج (production) كان من المفترض أن تلتقطه اختبارات الوحدة. هل يبدو الأمر مألوفًا؟
المشكلة هي أن تغطية الكود (code coverage) هي مقياس خادع. إنها تظهر أي أسطر من الكود تم تغطيتها، ولكنها لا تتحقق من جودة الاختبارات نفسها. يمكن أن يكون لديك تغطية 100%، ولكن بدون أي assert ذي معنى. وهنا يأتي دور الاختبار الطفري في C# - وهي منهجية "تكسر" الكود الخاص بك للتحقق من مدى قدرة اختباراتك على اكتشاف الأخطاء.
\n\nفي هذه المقالة، سنشرح ما هو الاختبار الطفري (mutation testing)، وكيفية تطبيقه في مشاريع .NET باستخدام Stryker.NET، ولماذا هو أمر لا غنى عنه لأي مطور C# جاد.
\n\nما هو الاختبار الطفري (Mutation Testing)؟
\n\nالاختبار الطفري هو طريقة لتقييم جودة الاختبارات، تعتمد على إدخال تغييرات صغيرة متعمدة (طفرات) في الكود المصدري للبرنامج. كل نسخة معدلة من الكود تسمى طافرة (mutant).
\n\nالمنطق بسيط:
\n- \n
- يتم أخذ كود الإنتاج الخاص بك. \n
- يتم إنشاء الطفرات تلقائيًا - على سبيل المثال، يتم استبدال العامل
+بـ-، ويتم تغيير الشرطif (x > 5)إلىif (x >= 5)، وtrueإلىfalse. \n - يتم تشغيل اختبارات الوحدة الخاصة بك ضد كل طافرة. \n
- إذا فشل الاختبار - يتم "قتل" الطافرة (killed). هذا اختبار جيد. \n
- إذا نجح الاختبار - "تنجو" الطافرة (survived). هذه علامة سيئة: اختبارك لم يلاحظ تغيير المنطق. \n
الهدف هو تحقيق نسبة عالية من الطفرات المقتولة (عادةً 80%+). هذا ضمان حقيقي بأن اختباراتك تتحقق من السلوك، وليس فقط "تمر" عبر الكود.
\n\nلماذا تغطية الكود (code coverage) غير كافية؟
\n\nلننظر إلى مثال بسيط بلغة C#. لنفترض أن لدينا طريقة تتحقق مما إذا كان الرقم زوجيًا:
\n\npublic class MathUtils\n{\n public bool IsEven(int number)\n {\n return number % 2 == 0;\n }\n}\n\nواختبار "مثالي" بتغطية 100%:
\n\n[Test]\npublic void IsEven_ShouldReturnTrueForEvenNumber()\n{\n var utils = new MathUtils();\n var result = utils.IsEven(4);\n Assert.IsTrue(result);\n}\n\nالاختبار ناجح، والتغطية 100%. ولكن ماذا لو قام شخص ما عن طريق الخطأ (أو عن قصد) بتغيير الطريقة إلى return number % 2 == 1;؟ هل سيفشل اختبارنا الوحيد؟ لا! سيتحقق من الرقم 4 (4 % 2 == 0، ولكن الطريقة سترجع false الآن)، سيفشل assert - سيتم قتل الطافرة. كل شيء على ما يرام؟ انتظر.
ماذا لو تم تغيير الكود إلى return number % 2 != 1;؟ منطقيًا هذا هو نفس الشيء، ولكن الاختبار الطفري سينشئ طافرة: return number % 2 != 0;. سيظل اختبارنا مع الرقم 4 ناجحًا (4 % 2 != 0 — false، الطريقة سترجع false، ولكن assert يتوقع true - سيفشل الاختبار). هذا جيد. لكن تخيل أننا كتبنا الاختبار بهذه الطريقة:
[Test]\npublic void IsEven_ShouldNotThrow()\n{\n var utils = new MathUtils();\n Assert.DoesNotThrow(() => utils.IsEven(4));\n}\n\nالتغطية لا تزال 100%، ولكن assert لا يتحقق من النتيجة! الآن أي طافرة ستبقى على قيد الحياة. سيظهر الاختبار الطفري على الفور أن اختباراتك فارغة.
\n\nأدوات الاختبار الطفري في C#
\n\nالأداة الرئيسية والقياسية فعليًا لـ .NET هي Stryker.NET. إنه إطار عمل مفتوح المصدر (open-source) يسهل دمجه في أي مشروع C#. وهو يدعم xUnit و NUnit و MSTest.
\n\nأدوات أخرى (أقل شهرة):
\n- \n
- VisualMutator - إضافة قديمة لـ Visual Studio. \n
- Nester