تكرار كود الاختبار.
يحدث تكرار كود الاختبار عندما يتم نسخ ولصق نفس كود الإعداد (setup) أو الإجراء (action) أو التحقق (assertion) عبر اختبارات متعددة، مما يعني أن تغييراً واحداً يفرض تعديلات في أماكن عديدة وتتحول الاختبارات إلى نسخ هشة متطابقة تقريباً.
##Signs and Symptoms
تتعرف على هذه المشكلة عندما تبدو الاختبارات وكأنها كُتبت بالنسخ واللصق بدلاً من إعادة الاستخدام:
- بناء نفس كائن الاختبار/الهيكل (fixture) حرفياً في بداية كل اختبار تلو الآخر.
- تكرار نفس تسلسلات التحقق (نفس 3-4 استدعاءات لـ
expectبنفس الترتيب) عبر الاختبارات. - من الواضح أنه تم استنساخ الاختبارات الجديدة من اختبار قديم مع تعديل سطر واحد فقط.
- ظهور نفس القيم الحرفية السحرية (magic literals) (المعرفات، روابط الويب، التواريخ، نصوص الأخطاء) مراراً وتكراراً.
- أي تغيير في دالة بناء واحدة أو توقيع API يؤدي إلى تعطل عشرات الاختبارات في نفس الوقت (Shotgun Surgery).
- رؤية اختبارات شبه مكررة تختلف فقط في المدخلات/القيم المتوقعة — وهو مرشح واضح للاختبارات المحددة بالمعاملات/القائمة على الجداول (parameterized/table test).
test('flight can be cancelled', () => {
const airport = new Airport('YYC', 'Calgary'); // مكرر
const flight = new Flight('AC123', airport, new Date('2026-06-01T10:00')); // مكرر
flight.cancel();
expect(flight.status).toBe('CANCELLED');
});
test('flight can be delayed', () => {
const airport = new Airport('YYC', 'Calgary'); // نسخ ولصق
const flight = new Flight('AC123', airport, new Date('2026-06-01T10:00')); // نسخ ولصق
flight.delay(30);
expect(flight.status).toBe('DELAYED');
});
##Reasons for the Problem
لماذا يحدث ذلك
- نسخ ولصق الاختبار السابق هو أسرع طريقة لكتابة الاختبار التالي.
- يتم التعامل مع الاختبارات على أنها "مواطن من الدرجة الثانية" — فلا يُعاد هيكلتها أو تطبيق نفس مبدأ عدم التكرار (DRY) المتبع في كود الإنتاج.
- عدم وجود هياكل اختبار مشتركة (shared fixtures)، أو دوال إنشاء (Creation Methods)، أو بناة بيانات اختبار (test-data builders)، وعدم إلمام المطورين بالاختبارات المحددة بالمعاملات أو القائمة على الجداول (parameterized/table-driven tests).
لماذا يضر ذلك
- قابلية الصيانة: يربط "ميسزاروس" (Meszaros) هذه المشكلة (smell) مباشرة بـ الاختبار الهش (Fragile Test) — فعندما "تظهر نفس تسلسلات الكود عدة مرات في اختبارات عديدة"، فإن تغييراً واحداً في كود الإنتاج يعني تعديل نفس الشيء في N من الأماكن. وتتزايد تكلفة الصيانة مع عدد النسخ المكررة، وليس مع عدد السلوكيات المختلفة.
- المقروئية: يؤدي تكرار الأكواد الجاهزة (boilerplate) إلى دفن السطر الوحيد الذي يجعل كل اختبار مختلفاً بالفعل، مما يمنع القارئ من رؤية ما يتم التحقق منه بوضوح وسرعة.
- الموثوقية: يدعو النسخ واللصق إلى ارتكاب أخطاء النسخ، ويتم تطبيق الإصلاحات على نسخة واحدة دون غيرها من النسخ المماثلة، مما يترك اختبارات غير متسقة ومتناقضة.
- الثقة الزائفة: إن أي تحقق (assertion) معيب يتم تكراره يصبح خاطئاً في العديد من الأماكن في نفس الوقت، وتتحرك الاختبارات المستنسخة بصمت بعيداً عن الغرض منها حتى تفقد صلتها بما تدعيه أسماؤها.
تنبيه — DRY مقابل DAMP: يفضل في الاختبارات أيضاً أن تكون عبارة عن عبارات وصفية وذات مغزى (DAMP - Descriptive And Meaningful Phrases). لا تبالغ في التجريد (abstraction) لدرجة يضطر معها القارئ إلى تتبع الدوال المساعدة لفهم اختبار واحد. قم باستخلاص التكرار الحقيقي الذي يكشف عن النية، مع الحفاظ على التفاصيل الأساسية لكل اختبار مرئية ومحلية.
##Treatment
تخلص من التكرار العرضي مع الحفاظ على وضوح جوهر كل اختبار:
- استخلص دوال مساعدة للاختبار / دوال إنشاء (Creation Methods) (مثل Object Mother، أو Test Data Builder) لعمليات بناء الكائنات المتكررة، بحيث يحدد كل اختبار القيم التي تهمه فقط.
- استخدم
beforeEach/ الإعداد الضمني (Implicit Setup) للسياق المشترك والملائم لكل اختبار في الكتلة البرمجية — ولكن تجنب إخفاء الحالة التي يعتمد عليها الاختبار (فذلك يستبدل مشكلة التكرار بمشكلة الاختبار الغامض - Obscure Test). - استخلص تحققات مخصصة (Custom Assertions) / دوال مساعدة للتحقق لتسلسلات التحقق المتكررة متعددة الخطوات، ويفضل أن تتحقق من شرط منطقي واحد.
- ادمج الاختبارات المتطابقة تقريباً في اختبارات محددة بالمعاملات / قائمة على الجداول (
it.each/test.each) بحيث تعيش أزواج المدخلات والمخرجات المتوقعة في جدول واحد. - استبدل القيم الحرفية السحرية المكررة بثوابت مسماة أو قيم افتراضية للباني (builder defaults).
قبل — بناء مكرر وثلاثة اختبارات متطابقة تقريباً:
test('rejects negative amount', () => {
expect(() => validateAmount(-1)).toThrow(RangeError);
});
test('rejects zero amount', () => {
expect(() => validateAmount(0)).toThrow(RangeError);
});
test('rejects NaN amount', () => {
expect(() => validateAmount(NaN)).toThrow(RangeError);
});
بعد — دالة الإنشاء تزيل تكرار الإعداد، والجدول يستبدل النسخ المستنسخة:
// دالة إنشاء مشتركة: تصرح الاختبارات فقط بالقيم المعدلة التي تهمها
const aFlight = (overrides = {}) =>
new Flight('AC123', new Airport('YYC', 'Calgary'),
new Date('2026-06-01T10:00'), overrides);
it.each([-1, 0, NaN])('rejects invalid amount %p', (amount) => {
expect(() => validateAmount(amount)).toThrow(RangeError);
});
قم بتشغيل أداة كشف النسخ واللصق (أدناه) على مصادر الاختبار الخاصة بك، ثم أعد هيكلة الكتل الأكبر والأكثر تكراراً أولاً.
##Detected by
- eslint-sonarjs no-identical-functions — لا ينبغي أن تحتوي الدوال على نفس التطبيق
- eslint-sonarjs no-duplicate-string — لا ينبغي تكرار السلاسل النصية الحرفية
- sonar javascript:S4144 — لا ينبغي أن تحتوي الدوال على نفس التطبيق
- pmd cpd — أداة كشف النسخ واللصق (CPD) — كتل الكود المكررة