اختبارات التطفير سلبية. واختبارات TDD إيجابية.
يُسمّى كلاهما اختبارًا، لكنهما يؤكدان أمرين متعاكسين. اختبار TDD يقرر ما يجب أن يفعله النظام، أما تشغيل التطفير فلا يستطيع إلا الإبلاغ عمّا لم تلاحظه مجموعة اختباراتك. ومعرفة أي قطب تحمله تغيّر ما ينبغي فعله بالنتيجة.
نوعان من التأكيد
الاختبار المدفوع بالاختبارات يُكتب قبل وجود السلوك. يفشل، ثم ينجح، ومن تلك اللحظة يصبح تصريحًا قائمًا: هذا المُدخل يجب أن ينتج تلك النتيجة. تتراكم المجموعة لتصبح مواصفة قابلة للقراءة، وكتابتها أولًا تضع ضغطًا على التصميم، لأن الكود الذي يصعب مناداته يصعب اختباره. أما تشغيل التطفير فيفعل شيئًا مختلفًا بنيويًا: يأخذ كودًا يعمل أصلًا، ويغيّره عن قصد، ويعيد تشغيل المجموعة. وكل نتيجة مصوغة بالنفي: هذا التعديل مرّ دون ملاحظة. ولا شيء في ذلك المُخرَج يقول ما الغرض من البرمجية.
مجموعة TDD خضراء هي مجموعة تصريحات عن النوايا. ودرجة تطفير عالية هي غياب نوع معيّن من الدليل ضد مجموعتك. واحد فقط من هذين مواصفة.
الاختبار الإيجابي
- + يُكتب قبل الكود، فيوجد المطلب بالكلمات قبل أن يوجد في المنطق
- + يفشل أولًا، وهذا الدليل الرخيص الوحيد على أن الاختبار قادر على الفشل أصلًا
- + يسمّي قاعدة، فينجو من إعادة الهيكلة: يمكن للداخليات أن تتحرك دون أن يتحرك التأكيد
- + يضع ضغطًا على التصميم بينما لا يزال تغييره رخيصًا
- + يُقرأ كتوثيق من الشخص التالي، ومن الوكيل التالي أيضًا
الفحص السلبي
- − يعمل لاحقًا، على كود واختبارات موجودة سلفًا
- − الطفرة المقتولة لا تنقل معلومة جديدة؛ الإشارة تحملها الطفرات الباقية فقط
- − الطفرة الباقية دليل على عمى، لا دليل على خلل في المنتج
- − لا تقول شيئًا عن المتطلبات الناقصة، بل عن عدم الحساسية للتغييرات التي جرّبها
- − تنتج تقريرًا وقائمة عمل، لا أثرًا يحفظه أحد
الكلمة نفسها، وأداتان مختلفتان
| الاختبار الإيجابي | الفحص السلبي | |
|---|---|---|
| ماذا يؤكد | يجب أن يتصرف النظام على هذا النحو. | مجموعتك لم تلاحظ هذا التغيير. |
| ماذا يثبت اللون الأخضر | كل سلوك تمّت مواصفته حتى الآن منفَّذ. | المجموعة حسّاسة لتلك المجموعة من العوامل. ولا شيء عن الصحة. |
| ماذا يتركه وراءه | اختبارات وتصميم تشكّل بفعل كتابتها. | تقرير. لا بد من استخلاص قيمته قبل أن تنتهي صلاحيته. |
| متى يعمل | باستمرار، بمقياس الدقائق، أثناء كتابة الكود. | في التكامل المستمر: تدريجيًا على الملفات المتغيرة أو مسحًا ليليًا. |
| نموذج الكلفة | يُدفع بانتظام ويُستوعب داخل التطوير. | عدد الطفرات مضروبًا بزمن الاختبارات المعنية. يكبر مع كليهما. |
| الأثر على التصميم | ضغط قابلية الاختبار. التصميم الأخرق يوجع فورًا. | لا شيء. إنه يقيّم ما هو موجود سلفًا. |
| كيف يأتي الفشل | أحمر، بقصد، بقلمك، خطوة خطوة. | طفرة باقية لم يكتبها أحد وينبغي الآن تفسيرها. |
الطفرة الباقية نفسها، واستجابتان
هنا يتوقف الحديث عن القطب ليصبح عملًا. تقرير التطفير يعطيك موقعًا، وما تكتبه بعده يحدد إن كانت المجموعة ستزداد قوة أم مجرد تصلّبًا.
الكود والطفرة التي تبقى
jsإزالة .trim() طفرة معتادة على نداء الدوال. تبقى حية كلما لم تحمل أي بيانات تجريبية مسافات محيطة، وهذا حال معظمها.
// importer.js
const normalise = (value) => value.trim().toLowerCase();
export function importRows(rows) {
return rows.map((row) => ({ email: normalise(row.email) }));
}
// Surviving mutant: normalise() with .trim() removed.
// The suite passes either way, so the report flags it. قتل الطفرة وربط المجموعة بالتنفيذ
jsهذا الاختبار يقتل الطفرة الباقية، لكنه يجمّد أيضًا التنفيذ الحالي: على normalise أن تبقى قابلة للوصول وأن تُنادى بهذا العدد بالضبط. أعِد تسميتها أو ادمجها أو انقلها خلف حدّ، وسينكسر الاختبار مع أن السلوك لم يتغير إطلاقًا.
it('calls normalise once per row', () => {
const spy = vi.spyOn(internals, 'normalise');
importRows([{ email: ' Ada@Example.COM ' }]);
expect(spy).toHaveBeenCalledTimes(1);
});
// Green. Mutant dead. Score up.
// Nothing here states what an imported email address should look like. الإجابة على السؤال الذي طرحته الطفرة
jsالطفرة نفسها والقتل نفسه، لكن التأكيد هنا قاعدة يمكن لمسؤول منتج أن يقرأها. يمرّ عبر الدالة العامة، فتبقى الداخليات حرة الحركة، ويشرح نفسه في رسالة الفشل بعد ستة أشهر.
it('trims and lower-cases every imported email address', () => {
expect(importRows([{ email: ' Ada@Example.COM ' }]))
.toEqual([{ email: 'ada@example.com' }]);
});
// Same mutant dead, but the suite gained a specification
// instead of a snapshot of today's call graph. إبقاء التدقيق رخيصًا بما يكفي لمواصلته
jsonتحليل التغطية لكل اختبار هو أكبر رافعة أداء: فهو لا يشغّل إلا الاختبارات التي تصل فعلًا إلى كل طفرة. والوضع التدريجي يبقي تشغيل طلب الدمج في دقائق. وعتبة break أرضية ضد التراجع، لا هدفًا يُطارد.
{
"testRunner": "vitest",
"coverageAnalysis": "perTest",
"incremental": true,
"mutate": ["src/**/*.js", "!src/**/*.test.js"],
"thresholds": { "high": 80, "low": 60, "break": 60 }
} حين يصبح السلبي سامًّا
مطاردة الدرجة
بمجرد أن تصبح درجة التطفير هدفًا، تُكتب اختبارات لقتل الطفرات بدلًا من إعلان المتطلبات. تمرّ في المراجعة لأنها خضراء، وهي بالضبط التي تنكسر في إعادة الهيكلة التالية مع أن السلوك لم يتغير.
محاربة الطفرات المكافئة
بعض الطفرات تغيّر الكود دون تغيير السلوك المرصود، ولا يمكن لأي اختبار صادق قتلها. وثّقها واستثنِها وامضِ. الوقت المصروف هنا يشتري رقمًا لا ثقة.
تأكيد الطفرة لا القاعدة
إن لم تستطع تسمية المطلب الذي يحميه اختبار جديد، فقد كتبت كاشف تغيير. سيبلّغ عن كل تعديل مستقبلي كفشل، وسيعلّم الفريق التوقف عن قراءة مُخرَجات الاختبار.
تشغيل كل شيء في كل مرة
المسح الكامل على قاعدة كود كبيرة مهمة ليلية لا بوابة لطلب الدمج. وجّه تشغيل الطلب إلى الملفات المتغيرة مع تحليل التغطية لكل اختبار، وأبعد المسح الشامل عن المسار الحرج.
حوّل كل طفرة باقية إلى تصريح إيجابي
اقرأ الطفرة الباقية كسؤال
- - أي سلوك يجب أن يكون صحيحًا كي يكسر هذا التغيير شيئًا؟
- - أي مطلب سيفشل الآن لو أن أحدًا كتبه؟
- - من في المراحل التالية سيلاحظ لو وصلت هذه الطفرة إلى الإنتاج؟
اكتب الجواب لا القتل
- - سمِّ الاختبار باسم القاعدة، لا باسم الطفرة ولا برقم السطر
- - أكِّد عبر السطح العام لتبقى الداخليات حرة في التغيير
- - إن لم يمكن الوصول إلى القاعدة إلا بكشف الداخليات، فتلك نتيجة تصميم لا نتيجة اختبار
أو احذف الكود
- - إن لم يمكن تسمية أي مطلب، فلا شيء يعتمد فعلًا على هذا السلوك
- - الطفرة الباقية في كود غير موصَّف غالبًا مطلب ميت أكثر من كونها اختبارًا ناقصًا
- - الحذف أرخص قتل ممكن، ويجعل التدقيق التالي أسرع
أحدهما يكتب المواصفة والآخر يدقّقها
اختبار التطفير ليس TDD أفضل ولا بديلًا عنه. فهو لا ينتج اختبارات ولا تصميمًا ولا إعلانًا للنية. ما ينتجه قائمة بالمواضع التي تكون فيها مواصفتك أقل حساسية مما افترضت، وهذا مفيد فعلًا ومختلف فعلًا.
لذا أبقِ توزيع الأدوار صريحًا. تُولد الاختبارات عبر TDD كتأكيدات إيجابية عن السلوك، وتجري تشغيلات التطفير دوريًا كتدقيق لتلك التأكيدات. ولا تذهب أي طفرة باقية إلى اختبار مباشرة: ترجمها أولًا إلى مطلب، أو احذف الكود الذي تسكنه. النتيجة السلبية لا تصبح قيمة باقية إلا حين يحولها أحدهم إلى تصريح إيجابي عن الغرض من البرمجية.
مقالات هندسية ذات صلة
للتغطية والتعقيد مقياسهما الخاص، وهناك الآن تدفق عمل بالوكلاء يسلّم التصليب إلى دور مخصص.
مقياس CRAP: التعقيد والتغطية في رقم واحد
كيف تجمع صيغة CRAP بين التعقيد والتغطية، وما تطلبه كل درجة، وكيف تجعلها بوابة دون إطلاق حملة تنظيف.
SwarmForge: ما يصيبه سرب وكلاء أنكل بوب
مراجعة لتقسيم الأدوار وعزل شجرة العمل وبروتوكول التسليم في SwarmForge، وأي أجزاء تنتقل إلى أي فريق.
الأسئلة الشائعة
هل اختبار التطفير أفضل من تغطية الكود؟
إنه يجيب على سؤال أقوى بثمن أعلى بكثير. التغطية تحسب إن كان السطر قد نُفِّذ، والتطفير يسأل إن كان شيء قد تأكّد فعلًا. استخدم التغطية كبوابة مستمرة رخيصة، والتطفير كتدقيق دوري على الكود المهم.
أي درجة تطفير ينبغي أن نستهدف؟
تُعدّ الدرجات فوق 80 بالمئة قوية عادة، و60 إلى 80 بالمئة صالحة مع فجوات حقيقية، لكن الرقم لا يعني شيئًا إلا على مستوى الوحدة. القاعدة الأنفع هي ألا يوجد تراجع في الملفات المتغيرة، مع عتبة break تمنع انحدار الدرجة.
هل يحل اختبار التطفير محل TDD؟
لا. اختبار التطفير لا يكتب شيئًا ولا يوصّف شيئًا. إنه يقيّم المجموعة الموجودة لديك، وكل نتيجة تحتاج قرارًا بشريًا عن المطلب الذي تشير إليه.
ما الأدوات التي تستخدمها الفرق؟
PIT هو الخيار الراسخ على منصة JVM، ويغطي Stryker Mutator لغات JavaScript وTypeScript وC# وScala، وفي بايثون الخياران المعتادان mutmut وcosmic-ray. وكلها تدعم تقييد التشغيل بالملفات المتغيرة، وهذا ما يجعل الممارسة محتملة الكلفة.
أين يقع اختبار التطفير في التكامل المستمر؟
تدريجيًا على طلبات الدمج، مقصورًا على الملفات المتغيرة مع تحليل التغطية لكل اختبار، بالإضافة إلى مسح كامل مجدول. أبلغ عن الطفرات الباقية كبنود مراجعة لا كإخفاقات بناء، واجعل البوابة على تراجع الدرجة فقط.