الاختبار المتجاهل.
اختبار تم رفعه إلى كود المشروع ولكنه لا يعمل أبداً لأنه تم تخطيه، أو تعطيله، أو تحويله إلى تعليق، مما يعطي مظهراً كاذباً بالتغطية دون التحقق من أي شيء فعلياً.
##العلامات والأعراض
يوجد اختبار في المجموعة ولكنه يُمنع بشكل دائم من التنفيذ. لا يزال يُحسب كـ "اختبار" في الملف، ولكنه يسهم بصفر من التحقق. راقب علامات التخطي/التجاهل، والكتل المسبوقة بحرف x، وكود الاختبار المدفون في التعليقات — وغالباً ما يقترن ذلك بعذر غامض مثل "متقلب (flaky)" أو "الإصلاح لاحقاً".
// معدلات التخطي/التعطيل — لا تعمل أبداً
describe.skip('checkout flow', () => { /* ... */ });
it.skip('applies the discount', () => { /* ... */ });
// مسبوقة بـ x — نفس التأثير
xit('rejects negative amounts', () => { /* ... */ });
xdescribe('payments', () => { /* ... */ });
// اختبار محول لتعليق — غير مرئي لبيئة التشغيل والمبلغ
// it('retries on 503', async () => {
// await expect(client.fetch()).resolves.toBeOk();
// });
// JUnit: @Ignore / @Disabled مع عذر واهٍ
@Ignore("disabled for now as this test is too flaky")
@Test public void peerPriority() { /* ... */ }
العلامات الدالة في تقرير الاختبار: وجود عدد غير صفري للاختبارات "المتخطاة"/"المعلقة" لا ينظر إليه أحد، ومجموعات اختبارات تتقلص بصمت بمرور الوقت، وعمليات تخطٍ غير مرتبطة بمشكلة تتبع أو تاريخ انتهاء صلاحية. الإرجاع الشرطي (return) في الجزء العلوي من الاختبار (مثل if (process.platform === 'win32') return;) هو بديل أكثر ذكاءً يخفي التخطي عن عدادات التخطي تماماً.
ليست كل علامة من هذا النوع مشكلة. فـtest.todo('handles expired coupons — PAY-412') بلا جسم، ويظهر في التقرير بوصفه todo (لا بوصفه اختبارًا ناجحًا)، ويشير إلى تذكرة متابعة: إنه عنصر نائب صريح وظاهر. أما الاختبار ذو الجسم الفارغ الذي يُبلَّغ عنه بالأخضر فمشكلة أخرى هي الاختبار الفارغ.
##أسباب المشكلة
لماذا يحدث ذلك
- بدأ اختبار ما بالفشل (تراجع حقيقي، أو تبعية توقيت متقلبة، أو تغيير في البيئة/الإصدار) وكان تخطيه هو أسرع طريقة للحصول على بناء ناجح (أخضر) أو إلغاء حظر دمج الأكواد.
- كُتب اختبار قبل التنفيذ الفعلي ورُكن باستخدام
xit/it.skipدون تذكرة متابعة، ولم يرجع إليه أحد. (أماtest.todoالمرتبط بتذكرة فيبقى على الأقل ظاهرًا في كل تقرير.) - أدت عمليات الهجرة (إلى إطار عمل جديد، أو بيئة تشغيل جديدة، أو واجهة برمجة تطبيقات جديدة) إلى كسر الاختبار وتم ركنه "مؤقتاً".
- كان المقصود من التخطي أن يستمر لفترة قصيرة؛ ولكن مع عدم وجود تذكير، أو تاريخ انتهاء، أو تذكرة تتبع، أصبح التخطي دائماً.
لماذا يضر ذلك
- الثقة الزائفة. يبدو الاختبار المتخطى كجزء من التغطية في الملف وفارق طلب الدمج (PR diff)، ولكنه لا يشغل شيئاً. أصبح مسار الكود الذي ادعى الاختبار حمايته غير محمي الآن، بينما يفترض الجميع أنه آمن.
- عفن الكود (Bit rot). لا يزال الاختبار المتخطى يُصرَّف ويخضع لفحص الأنواع، لكنه لا يُنفَّذ أبدًا، فلا يلاحظ أحد متى تتوقف تجهيزاته وقيمه المتوقعة وافتراضاته عن مطابقة الكود. والاختبار المحوَّل إلى تعليق أسوأ: فهو لا يُصرَّف أصلًا، لذا تكسره تغييرات الواجهة البرمجية بصمت ولا يكتشف أحد ذلك حتى يُزال التعليق عنه. وكلما طال بقاء أيٍّ منهما ابتعد عن الواقع، حتى تصبح إعادة تفعيله أكثر كلفة من إعادة كتابته.
- التراجعات المخفية. لا يزال الخطأ أو السلوك المتقلب الذي دفع إلى التخطي موجوداً — لقد تم إسكاته فقط. التخطي يعالج العَرَض (السطر الأحمر) بدلاً من علاج المرض.
- الضوضاء والتآكل. تدرب أرقام الاختبارات "المتخطاة" المستمرة الفريق على تجاهل تقرير الاختبار، مما يسمح بمرور عمليات تخطٍ جديدة دون أن يلاحظها أحد. كما أن كود الاختبار الميت والمحول لتعليق يضيف عبء قراءة وصيانة إضافياً دون أي فائدة.
##المعالجة
تعامل مع كل اختبار متجاهل كقرار يجب اتخاذه الآن، وليس كأمر يتم تأجيله إلى أجل غير مسمى.
- فرز كل تخطٍ. لكل اختبار معطّل أو محوَّل إلى تعليق، قرر: إصلاحه، أو حذفه، أو وضعه في الحجر الصحي مع تاريخ انتهاء وتذكرة تتبع. خيار "تركه متخطى للأبد" ليس خياراً مطروحاً. أما
test.todoالذي يذكر تذكرة فهو عنصر نائب صريح بالفعل، فأبقِه ما دامت التذكرة مفتوحة فقط. - الإصلاح وإعادة التفعيل إذا كان السلوك لا يزال مهماً. إذا كان الاختبار متقلباً، فأصلح التقلب (التحكم في الوقت، والعشوائية، والعمليات غير المتزامنة، والحالة المشتركة) بدلاً من تخطيه.
- احذفه إذا كانت الميزة قد أزيلت أو كان الاختبار قديماً. الاختبار المحذوف يعبر عن الصدق; بينما الاختبار المتخطى يكذب. يتذكر نظام التحكم في الإصدارات (Git) الاختبار إذا احتجت إليه مجدداً — لذا لا تقم أبداً بتحويل اختبار إلى تعليق بدلاً من حذفه.
- إذا كان لا بد من التخطي مؤقتاً، فاجعله واضحاً ومحدداً بزمن: قم دائماً بتضمين سبب ورابط تتبع، ويفضل استخدام آلية تظهر في التقارير وتفشل بمجرد انتهاء الموعد النهائي، حتى لا يتعفن التخطي.
- أوقف النزيف باستخدام التحليل الساكن والتكامل المستمر (Lint/CI). قم بتفعيل قاعدة "منع الاختبارات المعطلة" ليتم كشف التخطيات الجديدة في مراجعة الكود، وتعامل مع عدد التخطيات الحالي كعمل متراكم (backlog) يجب خفضه إلى الصفر.
// قبل — صامت، دائم، غير متتبع
it.skip('refunds the full amount on cancel', async () => {
await expect(refund(order)).resolves.toEqual({ amount: 100 });
});
// بعد — تم إصلاحه ويعمل مجدداً (السبب الجذري: المجموع بالفاصلة العائمة)
it('refunds the full amount on cancel', async () => {
await expect(refund(order)).resolves.toEqual({ amount: 100 });
});
// بديل مؤقت مقبول — مرئي، منسوب لسبب، وينتهي صلاحيته
it.skip('refunds the full amount on cancel — flaky clock, see JIRA-1234 (remove by 2027-03-01)', async () => {
/* ... */
});
// قبل
@Ignore("too flaky")
@Test public void peerPriority() { ... }
// بعد: أصلح التبعية الزمنية وأعد تفعيله، أو احذفه إن كان قديماً
@Test public void peerPriority() { ... }
##أدوات الكشف
- eslint-jest jest/no-disabled-tests — منع الاختبارات المعطلة
- eslint-jest jest/no-commented-out-tests — منع الاختبارات المحولة إلى تعليقات
- eslint-vitest vitest/no-disabled-tests — منع الاختبارات المعطلة
- eslint-vitest vitest/no-commented-out-tests — منع الاختبارات المحولة إلى تعليقات
- sonar java:S1607 — يجب أن تذكر @Ignore / @Disabled سببًا؛ ولا تُبلغ القاعدة إلا عن التعليقات التوضيحية التي تخلو منه
- sonar javascript:S1607 — يجب عدم تخطي الاختبارات دون تقديم سبب