الوحدة 5 · استراتيجية الاختبار
اختبار الطفرات (Mutation Testing)
20 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
تشرح كيف يقيّم الـ mutation testing مجموعة الاختبارات، وتشغّل StrykerJS مع Vitest على المنطق الأساسي، وتحوّل الـ mutants الناجية إلى الاختبارات المحدّدة التي كانت ناقصة.
- 1كيف يعمل
- 2تشغيل StrykerJS مع Vitest
- 3قراءة الناجين
- 4الـ mutants المنتقاة يدويًا مقابل المولّدة
- 5أين يناسب
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
ترك لك التمرين الأخير حقيقة مزعجة: قالت الـ coverage إن النسبة 100%، ولم تلتقط المجموعة شيئًا. تحتاج إلى قياس يطرح السؤال الحقيقي: هل ستلاحظ هذه الاختبارات لو كان الكود خاطئًا؟ يجيب الـ mutation testing (اختبار الطفرات) عن هذا السؤال بالتجربة. وقد كنت تفعل ذلك يدويًا أصلًا: كل تمرين في هذه الدورة شغّل اختباراتك على “mutants” (نسخ طافرة). والأداة تفعل الشيء نفسه بشكل منهجي، لكل سطر.
كيف يعمل
- تصنع الأداة mutant: نسخة من كودك فيها تغيير صغير واحد، من النوع الذي يزلّ فيه الإنسان. يصبح
<هو<=، و+يصبح-، وMath.minيصبحMath.max، ويصبح شرط ماtrue، وتُفرَّغ كتلة كود. - تشغّل الاختبارات التي تغطّي ذلك السطر على الـ mutant.
- إن فشل أيّ اختبار، يُقتَل الـ mutant: اختباراتك ستلتقط ذلك الخطأ. وإن نجحت كل الاختبارات، فقد نجا: ذلك الخطأ سيصل إلى المستخدمين.
تكرّر هذا مع مئات أو آلاف الـ mutants. ومقياس الطفرات (mutation score) هو نسبة المقتول منها.
تسأل الـ coverage “هل عمل هذا السطر؟” ويسأل الـ mutation testing “لو كان هذا السطر خاطئًا، هل سيلاحظ أحد؟” والسؤال الثاني هو الذي يهمّك.
تشغيل StrykerJS مع Vitest
StrykerJS هي أداة الـ mutation testing الراسخة لـ JavaScript وTypeScript، ولها plugin مشغّل لـ Vitest:
npm install --save-dev @stryker-mutator/core @stryker-mutator/vitest-runner
npx stryker init # interactive; or write the config yourself{
"$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"testRunner": "vitest",
"plugins": ["@stryker-mutator/vitest-runner"],
"mutate": ["src/money/**/*.js", "!src/**/*.test.js"],
"reporters": ["html", "clear-text", "progress"],
"thresholds": { "high": 80, "low": 60, "break": 50 },
"incremental": true
}يشغّل npx stryker run مجموعتك أولًا مرة واحدة ليعرف أيّ الاختبارات تغطّي أيّ الأسطر، ثم يختبر كل mutant. ويعرض تقرير HTML الكود المصدري مع تعليم كل mutant بأنه قُتل أو نجا. ويُفشل thresholds.break التشغيل إن كان المقياس دونه، ويعيد incremental استخدام النتائج للكود والاختبارات التي لم تتغيّر منذ التشغيل الأخير، وهذا مهم جدًا للسرعة.
قراءة الناجين
الحالات التي ستراها أكثر من غيرها:
- Killed (مقتول): فشل اختبار. جيد.
- Survived (ناجٍ): نجح كل اختبار يغطّيه. assertion ناقص أو ضعيف.
- No coverage (بلا تغطية): لم يشغّل أيّ اختبار ذلك السطر أصلًا. وكانت الـ coverage ستخبرك بهذا أيضًا.
- Timeout (انتهاء المهلة): سبّب الـ mutant حلقة لا نهائية فانتهت مهلة الاختبارات. ويُحسب على أنه مكتشَف.
الناجون هم الكنز. بالنسبة إلى settleUp، قد يقول التقرير:
[Survived] MethodExpression src/money/settle-up.js:21
- const amountCents = Math.min(debtors[d].cents, creditors[c].cents);
+ const amountCents = Math.max(debtors[d].cents, creditors[c].cents);
[Survived] ConditionalExpression src/money/settle-up.js:4
- if (sum !== 0) throw new Error('Balances must add up to zero');
+ if (false) throw new Error('Balances must add up to zero');كل واحد منها تعليمة دقيقة. الأول يقول إن لا اختبار يستخدم مبالغ يدين فيها المدين بمجموع مختلف عمّا يستحقّه الدائن، فيعطي min وmax الإجابة نفسها في كل اختبار. والثاني يقول إن لا اختبار يمرّر مدخلات غير متوازنة أبدًا. اكتب هذين الاختبارين فيُقتل الاثنان معًا.
الـ mutants المنتقاة يدويًا مقابل المولّدة
الـ mutants في تمارين هذه الدورة انتُقيت يدويًا: كل واحد منها يحاكي خطأً يكتبه الناس فعلًا، مثل معامل متبادل الموقع أو await منسيّة. أما mutants الـ Stryker فتُولَّد بشكل منهجي من مجموعة ثابتة من المحوِّرات (mutators)، عادةً عشرات منها لكل دالة، فتجد فجوات لم تكن لتخطر لك، وتُنتج أيضًا بعض الضجيج. كلاهما مفيد. حين تراجع اختبارات زميل، فعادة الانتقاء اليدوي هي النسخة السريعة: اسأل “ما الخطأ الأرجح هنا، وأيّ اختبار سيفشل؟” وحين تريد قياسًا على مستوى وحدة كاملة، دع الأداة تولّد القائمة.
أين يناسب
الـ mutation testing بطيء: مئات الـ mutants، كل منها يشغّل جزءًا من مجموعتك. بضع عادات تُبقيه عمليًا:
- احصره بـ
mutateفي الكود الذي تكون أخطاؤه مكلفة: المال، والصلاحيات، والتحليل. لا في كود ربط الواجهة. - شغّله تدريجيًا محليًا، ووفق جدول زمني (ليليًا) أو على الملفات المتغيّرة في CI، بدل تشغيله على المستودع كله مع كل push.
- استخدم التقرير في المراجعة: الـ pull request الذي يضيف منطقًا وmutants ناجية يستحق سؤالًا.
في التمرين تحصل على مجموعة ضعيفة لـ settleUp وقائمة بالناجين، وتكتب الاختبارات التي تقتلهم.
أهم النقاط
- تصنع أداة الـ mutation testing (اختبار الطفرات) أخطاءً صغيرة متعمَّدة (mutants) في كودك وتعيد تشغيل الاختبارات؛ والـ mutant الذي يُفشل اختبارًا يُقتَل (killed)، والذي لا يُفشل أيّ اختبار ينجو (survived).
- مقياس الطفرات (mutation score)، أي نسبة الـ mutants المقتولة من مجموعها، يقيس كم من الأخطاء المعقولة ستلتقطها اختباراتك، وهذا ما لا تستطيعه الـ coverage.
- كل mutant ناجٍ يشير إلى assertion ناقص أو ضعيف؛ اقرأه، ثم اكتب الاختبار الذي يقتله.
- بعض الـ mutants متكافئة (لا تغيّر السلوك) ولا يمكن قتلها؛ تجاهلها بدل ليّ الاختبارات.
- الـ mutation testing بطيء، فشغّله على المنطق الأساسي، بشكل تدريجي أو وفق جدول زمني، لا على كل ملف في كل commit.