روليت التحقق.
يجمع اختبار واحد العديد من التحققات (assertions) غير الموثقة في طريقة واحدة، بحيث عندما يفشل لا يمكنك تحديد أي من التحققات قد فشل أو لماذا - مما يضطرك للمقامرة لمعرفة ذلك.
##Signs and Symptoms
يحتوي اختبار واحد على سلسلة من التحققات المجردة دون رسائل توضيحية وبدون هدف واحد واضح. عندما يفشل الاختبار (يصبح أحمر)، فإن تقرير الفشل (خاصة سطر ملخص التكامل المستمر CI أو مساعد التحقق المشترك) يخبرك بأن شيئاً ما قد تعطل، ولكن ليس أي فحص تعطل أو لماذا — فتضطر إلى لعب "الروليت" للعثور على السبب.
العلامات الدالة:
- العديد من استدعاءات
expect(...)/assert(...)في اختبار واحد، ولا يحمل أي منها رسالة توضيحية أو تسمية. - اسم الاختبار عام (
'works','user profile','test register') لذا فإن الفشل لا يوضح أي شيء بمفرده. - تغطي التحققات عدة اهتمامات غير مترابطة (مثل الاختبار المتلهف الذي يعيد استخدام تجهيز واحد لفحص كل شيء دفعة واحدة).
- يجعلك الفشل تلجأ إلى مصحح الأخطاء (debugger) أو أرقام الأسطر لمعرفة ما كان يتم التحقق منه بالفعل.
- نظراً لأن معظم التحققات تفشل سريعاً (fail-fast)، فإن الفشل الأول يوقف الباقي، وبالتالي لا ترى الفحوصات اللاحقة في تشغيل واحد.
test('user profile', () => {
const user = createUser({ name: 'Ada', age: 36 });
expect(user.name).toBe('Ada');
expect(user.age).toBe(36);
expect(user.isAdult).toBe(true); // إذا كان هذا هو الجزء الأحمر،
expect(user.slug).toBe('ada'); // فإن التقرير يكتفي بقول
expect(user.roles).toContain('member'); // "expected false to be true"
expect(user.createdAt).toBeInstanceOf(Date);
});
ملاحظة: بيئات التشغيل الحديثة (Jest, Vitest) تطبع السطر الفاشل والفروقات (diff)، مما يخفف من مشكلة "أي سطر؟". ولكن تظل الرائحة مؤثرة عندما يخلط الاختبار بين الأهداف، أو يحمل اسماً غامضاً، أو يكرر التحققات في حلقة مفرغة، أو يخفيها خلف مساعدين مشتركين، أو يشتغل في بيئة لا ينجو فيها سوى ملخص الرسالة فقط.
##Reasons for the Problem
لماذا يحدث ذلك
- الاختبار المتلهف / إعادة استخدام التجهيزات. تغريك عملية التجهيز المكلفة بإضافة فحوصات إضافية "بما أنني هنا" بدلاً من كتابة اختبار ثانٍ.
- التحقق من الكائن بأكمله بالطريقة الصعبة. التحقق من الكائن حقلاً بحقل يؤدي بطبيعة الحال إلى سلسلة طويلة من التحققات غير الموثقة.
- النمو عبر النسخ واللصق. تتراكم التحققات في الاختبارات بمرور الوقت دون أن يقوم أحد بتقسيمها.
- الأدوات القديمة تاريخياً. كانت تحققات xUnit الكلاسيكية تبلغ فقط بالنجاح أو الفشل، وبالتالي فإن التحقق غير المسمى لم يكن يقدم أي سياق عند الفشل - وهو أصل هذه الرائحة التي وصفها ميسزاروس (Meszaros) في البداية.
- ضغط المواعيد النهائية. كتابة اختبار واحد كبير تبدو أسرع من كتابة عدة اختبارات مركزة.
لماذا يضر ذلك
- القدرة على التشخيص / الموثوقية. وفقاً لموقع testsmells.org، "تؤثر عبارات التحقق المتعددة في طريقة الاختبار دون رسالة توضيحية على سهولة القراءة/الفهم/الصيانة حيث لا يمكن فهم سبب الفشل." تضيع الوقت في تحديد أي تحقق هو الذي فشل.
- العيوب المخفية (الثقة الزائفة). تتوقف التحققات سريعة الفشل (Fail-fast) عند أول فشل، لذلك لا يتم تنفيذ التحققات اللاحقة أبداً. تقوم بإصلاح واحد، ثم تعيد التشغيل، لتجد التالي - وبذلك تُحجب أخطاء متعددة، ويبدو ظهور اللون الأخضر بعد إصلاح واحد أكثر أماناً مما هو عليه في الواقع.
- سهولة القراءة. يتوقف الاختبار عن توثيق سلوك واحد؛ ويدفن الغرض منه في قائمة، ولا يضيف اسم الاختبار العام أي شيء.
- سهولة الصيانة. من غير الواضح ما إذا كان الفحص الجديد ينتمي إلى هذا الاختبار أم إلى اختبار جديد، لذا تستمر الكومة في النمو وتصبح الاهتمامات غير المترابطة مقترنة.
##Treatment
احرص على تطبيق المبدأ الكامن وراء اختبار الشرط الواحد: يتحقق كل اختبار من سلوك واحد ويمكن أن يفشل لسبب واحد فقط.
- التقسيم حسب الهدف. قسّم الاختبار الشامل إلى اختبارات مركزة بأسماء وصفية. وبذلك يصبح الاسم نفسه هو رسالة الفشل.
- دمج الفحوصات الحقلية في تحقق واحد. استخدم
toEqual/expect.objectContaining/ لقطة شاشة تمت مراجعتها (snapshot) بحيث تتحول تحققات N إلى فرق واحد (diff) ذي مغزى. - عندما يكون التجميع متماسكاً بالفعل، ضع تسميات للتحققات. لا تحتوي
expectفي Jest/Vitest على وسيط للرسالة، لذا استخدم وسيط الرسالة فيnode:assert، أوexpect(actual, message)في Vitest، أوjest-expect-message. - هل تريد تشغيل كل الفحوصات والإبلاغ عنها معاً؟ يفضل تقسيمها؛ وإلا استخدم التحققات المرنة (soft assertions مثل
expect.softفي Vitest) حتى لا يخفي فشل واحد بقية التحققات. - احمِ الكود من التراجع (regression) عن طريق وضع حد أقصى للتحققات لكل اختبار (انظر كواشف الرائحة).
قبل — روليت التحقق:
test('register user', () => {
const res = register({ email: 'a@b.com', age: 36 });
expect(res.ok).toBe(true);
expect(res.user.email).toBe('a@b.com');
expect(res.user.isAdult).toBe(true);
expect(res.welcomeEmailSent).toBe(true);
});
بعد — سلوك واحد لكل اختبار، مع تحقق لكامل الكائن:
describe('register', () => {
it('accepts a valid adult signup', () => {
expect(register({ email: 'a@b.com', age: 36 }).ok).toBe(true);
});
it('stores the normalized user record', () => {
const { user } = register({ email: 'a@b.com', age: 36 });
expect(user).toEqual(
expect.objectContaining({ email: 'a@b.com', isAdult: true }),
);
});
it('sends a welcome email on signup', () => {
expect(register({ email: 'a@b.com', age: 36 }).welcomeEmailSent).toBe(true);
});
});
إذا كان يجب عليك الاحتفاظ باختبار واحد، فاجعل كل تحقق يصف نفسه بنفسه:
import { strict as assert } from 'node:assert';
assert.equal(res.ok, true, 'registration should succeed');
assert.equal(res.welcomeEmailSent, true, 'welcome email should be sent');
##Detected by
- eslint-jest jest/max-expects — يحدد المؤشر المعتمد على العدد لروليت التحقق: يبلغ عندما يتجاوز الاختبار N من استدعاءات expect() (افتراضياً 5). تشير التوثيقات إلى أن زيادة التحققات تؤدي غالباً إلى خلط أهداف متعددة.
- eslint-vitest vitest/max-expects — مكافئ Vitest: يفرض حداً أقصى لعدد تحققات expect() لكل اختبار، مما يكشف الاختبارات التي تحتوي على عدد كبير جداً من الفحوصات.
- sonar java:S5961 — قاعدة SonarSource 'يجب ألا تحتوي طرق الاختبار على عدد كبير جداً من التحققات' - يضع حداً أقصى للتحققات لكل اختبار (افتراضياً 25 لـ JUnit/AssertJ)، وهو المؤشر القياسي للتحليل الساكن لهذه الرائحة.