الاختبار المحدد بشكل مفرط.
يتحقق الاختبار المحدد بشكل مفرط من تفاصيل تفوق بكثير ما يتطلبه السلوك قيد الاختبار — مثل تثبيت نصوص مخرجات دقيقة، أو هياكل كائنات كاملة، أو ترتيب المجموعات، أو كل استدعاء للمتعاونين الداخليين — مما يجعله ينكسر كلما تغير تفصيل تنفيذي غير ذي صلة.
##Signs and Symptoms
يمكنك التعرف على الاختبار المحدد بشكل مفرط من خلال ما يكسره: تؤدي إعادة هيكلة غير ضارة أو تغيير عارض إلى فشل الاختبارات على الرغم من أن السلوك الذي تصفه لا يزال صحيحاً.
الإشارات الشائعة:
- التحقق من تفاصيل غير مهمة. مثل المسافات الفارغة/الترميز الدقيق، أو نصوص رسائل الخطأ الكاملة، أو نصوص السجل (log)، أو التنسيق، أو القيم المتقلبة مثل الطوابع الزمنية، والمعرفات الفريدة (UUIDs)، والمعرفات التلقائية.
- تطابق الكائن بأكمله بينما يهمنا حقل واحد فقط. مثل استخدام
toEqual({...the entire object...})عندما يدور الاختبار في الحقيقة حول خاصية واحدة فقط. - تحققات حساسة للترتيب على بيانات غير مرتبة منطقياً. مثل تثبيت ترتيب مجموعة، أو مفاتيح خريطة، أو نتائج استعلام لا يضمن العقد ترتيبها.
- التحقق من سلوك المتعاونين الداخليين. المحاكيات التي تتحقق من أعداد الاستدعاءات الدقيقة، والوسائط وسيطاً وسيطاً، وترتيب استدعاء التبعيات الداخلية بدلاً من النتيجة النهائية الملاحظة.
- لقطات شاشة (snapshots) عملاقة تلتقط شجرة العرض بأكملها أو جسم الاستجابة كاملاً، بحيث يفرض أي تغيير متداخل إعادة التسجيل.
- الوصول للتفاصيل الداخلية للتنفيذ. استعلام الحالة الخاصة أو بنية DOM (مثل
container.querySelector، والحقول الداخلية) بدلاً من السلوك العام الملاحظ من قبل المستخدم.
// محدد بشكل مفرط: يثبت الترميز الدقيق، وطابعاً زمنياً متقلباً، وكل استدعاء للمتعاونين
test('renders user badge', () => {
const html = renderBadge(user);
expect(html).toBe('<span class="badge badge--admin">Ada Lovelace</span>'); // ترميز دقيق
expect(analytics.track).toHaveBeenCalledTimes(1); // عدد الاستدعاءات الداخلية
expect(analytics.track).toHaveBeenCalledWith('badge_render', { ts: 1718445693221 }); // متقلب
});
اختبار حاسم مفيد: إذا كان بإمكانك تغيير التنفيذ دون تغيير السلوك الموثق وظل الاختبار يفشل، فهو محدد بشكل مفرط.
##Reasons for the Problem
لماذا يحدث ذلك
- عقلية "المزيد من التحققات = اختبار أكثر شمولاً": يثبت المطورون كل ما يمكنهم رؤيته، خلطاً بين الاكتمال و الصحة.
- تجعل الأدوات التحديد المفرط هو الطريق الأسهل: حيث تسجل دالة
toMatchSnapshot()المخرجات بأكملها، وتجعل أطر المحاكاة من السهل التحقق من كل تفاعل (مثلtoHaveBeenCalledWith، وترتيب الاستدعاءات، وعددها). - نسخ ولصق كائن حقيقي في
toEqualبدلاً من التحقق فقط من الحقل الذي يدور حوله الاختبار. - الاختبار من خلال خطافات تنفيذ ملائمة (مثل عقد DOM، والحقول الخاصة) بدلاً من العقد العام.
لماذا يضر ذلك
في كتاب xUnit Test Patterns لجرارد ميسزاروس، هذا هو السبب الجذري وراء رائحة الاختبار الهش (Fragile Test)، المنسوبة إلى البرمجيات المحددة بشكل مفرط (Overspecified Software) و حساسية السلوك (Behavior Sensitivity): حيث تقترن الاختبارات بـ كيفية عمل الكود بدلاً من ما ينتجه، لذا تفشل عند إجراء تغييرات لا تؤثر على السلوك.
- سهولة الصيانة. تؤدي كل عملية إعادة هيكلة للكود (refactoring) إلى سلسلة متتالية من إخفاقات الاختبارات غير ذات الصلة، مما يجعل الحفاظ على نجاح مجموعة الاختبارات مكلفاً ويمنع بنشاط إعادة هيكلة الكود.
- الموثوقية / الثقة. الاختبارات التي تطلق "إنذارات كاذبة" على كود صحيح تدرب الفريق على تجاهل الإخفاقات أو إعادة تسجيل اللقطات (snapshots) بشكل أعمى دون التحقق منها.
- الثقة الزائفة. عادة ما يتزامن التحديد المفرط مع قلة التحقق من النتيجة ذات المغزى: فيمكن للاختبار تثبيت طابع زمني وسطر سجل (log) دون فحص القيمة التي تهم المستخدم بالفعل. وكما توضح أدبيات اختبار التفاعل، فإن التحديد المفرط هو "التحقق من أشياء ليست جزءاً من النتيجة النهائية" — وغالباً ما يكون ذلك عن طريق التحقق من التفاعلات بدلاً من النتائج.
- سهولة القراءة. جدار من التحققات العارضة يحجب السلوك الوحيد الذي يهدف الاختبار إلى توثيقه.
##Treatment
تحقق فقط مما يتطلبه السلوك قيد الاختبار — ويفضل التحقق من النتيجة النهائية على التفاعلات الداخلية.
- سمِّ النتيجة الوحيدة التي يوثقها الاختبار وتحقق منها، ولا شيء أكثر. قسّم النتائج المختلفة تماماً إلى اختبارات منفصلة مسماة جيداً بدلاً من استخدام تحقق ضخم واحد.
- استخدم مطابقات جزئية/مرنة بدلاً من تطابق الكائن بأكمله: مثل
expect.objectContaining، أوtoMatchObject، أوarrayContaining، أوstringContaining/stringMatching، وexpect.any(...)للحقول التي لا يمكنك أو لا ينبغي لك تثبيتها. - قم بتحييد البيانات المتقلبة (الطوابع الزمنية، المعرفات، القيم العشوائية) باستخدام مطابقات الخصائص (مثل
expect.any(String)) أو بحقن ساعة/مولد معرفات ثابت — ولا تكتب قيمة اليوم يدوياً بشكل ثابت. - ألغِ افتراضات الترتيب للبيانات غير المرتبة: تحقق من الانتماء للمجموعة (
arrayContaining) أو قم بالفرز قبل المقارنة. - فضل التحقق من الحالة على التحقق من السلوك. استبدل الاستعلامات (queries) بـ stubs (ولا تتحقق منها); وتحقق فقط من استدعاء المتعاون عندما يكون هذا الاستدعاء هو التأثير الجانبي الملاحظ (أمر command)، وتجنب التحقق من الأعداد الدقيقة/الترتيب للمتعاونين الداخليين البحتين.
- اجعل لقطات الشاشة (snapshots) صغيرة ومقصودة، أو استبدل لقطة الشاشة المترامية الأطراف ببعض التحققات المحددة. استعلم باستخدام واجهة برمجة التطبيقات المواجهة للمستخدم (مثل
getByRole/getByTextفي Testing Library) بدلاً من الوصول لـcontainer/عقد DOM.
قبل ← بعد:
// قبل: يقرن الاختبار بالحقول المتقلبة وتسلسل الاستدعاءات الداخلية
expect(result).toEqual({
id: '8f3c-92a1-...', // معرف فريد (uuid) عشوائي
createdAt: '2026-06-15T10:01:33.221Z',// Date.now()
name: 'Ada Lovelace',
role: 'admin',
});
expect(logger.info).toHaveBeenCalledTimes(3);
expect(db.connect).toHaveBeenCalledBefore(db.query);
// بعد: التحقق فقط من السلوك الذي يدور حوله هذا الاختبار
expect(result).toMatchObject({ name: 'Ada Lovelace', role: 'admin' });
// المعرف وتاريخ الإنشاء عارضان؛ التسجيل وترتيب الاستدعاءات هما تفاصيل تنفيذية — لا تثبتهما
##Detected by
- eslint-jest jest/no-large-snapshots — no-large-snapshots
- eslint-vitest vitest/no-large-snapshots — no-large-snapshots
- eslint-testing-library testing-library/no-node-access — no-node-access
- eslint-testing-library testing-library/no-container — no-container