شعار ConstructiCat
CodeBust.
تصفح القسم ▾

الاختبار المحدد بشكل مفرط.

يتحقق الاختبار المحدد بشكل مفرط من تفاصيل تفوق بكثير ما يتطلبه السلوك قيد الاختبار — مثل تثبيت نصوص مخرجات دقيقة، أو هياكل كائنات كاملة، أو ترتيب المجموعات، أو كل استدعاء للمتعاونين الداخليين — مما يجعله ينكسر كلما تغير تفصيل تنفيذي غير ذي صلة.

##العلامات والأعراض

يمكنك التعرف على الاختبار المحدد بشكل مفرط من خلال ما يكسره: تؤدي إعادة هيكلة غير ضارة أو تغيير عارض إلى فشل الاختبارات على الرغم من أن السلوك الذي تصفه لا يزال صحيحاً.

الإشارات الشائعة:

  • التحقق من تفاصيل غير مهمة. مثل المسافات الفارغة/الترميز الدقيق، أو نصوص رسائل الخطأ الكاملة، أو نصوص السجل (log)، أو التنسيق، أو القيم المتقلبة مثل الطوابع الزمنية، والمعرفات الفريدة (UUIDs)، والمعرفات التلقائية.
  • تطابق الكائن بأكمله بينما يهمنا حقل واحد فقط. مثل استخدام toEqual({...the entire object...}) عندما يدور الاختبار في الحقيقة حول خاصية واحدة فقط.
  • تحققات حساسة للترتيب على بيانات غير مرتبة منطقياً. مثل تثبيت ترتيب مجموعة، أو مفاتيح خريطة، أو نتائج استعلام لا يضمن العقد ترتيبها.
  • التحقق من سلوك المتعاونين الداخليين. المحاكيات التي تتحقق من أعداد الاستدعاءات الدقيقة، والوسائط وسيطاً وسيطاً، وترتيب استدعاء التبعيات الداخلية بدلاً من النتيجة النهائية الملاحظة.
  • لقطات شاشة (snapshots) عملاقة تلتقط شجرة العرض بأكملها أو جسم الاستجابة كاملاً، بحيث يفرض أي تغيير متداخل إعادة التسجيل.
  • مطابِقات (matchers) مفرطة الدقة. استخدام toStrictEqual حيث يكفي toMatchObject، أو المساواة التامة بين أعداد الفاصلة العائمة بدلًا من toBeCloseTo، أو نص الرسالة كاملًا حيث تكفي عبارة مفتاحية (toThrow(/expired/)) للتعبير عن المتطلب.
// محدد بشكل مفرط: يثبت الترميز الدقيق، وطابعاً زمنياً متقلباً، وكل استدعاء للمتعاونين
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 }); // متقلب
});

اختبار حاسم مفيد: إذا كان بإمكانك تغيير التنفيذ دون تغيير السلوك الموثق وظل الاختبار يفشل، فهو محدد بشكل مفرط.

روائح مجاورة: التحقق من بنية DOM أو الحالة الخاصة بدلًا مما يستطيع المستخدم أو الكود المستدعي ملاحظته هو اختبار تفاصيل التنفيذ؛ والاختبار الغارق في إعداد كائنات المحاكاة هو المحاكاة المفرطة. أما الاختبار المحدد بشكل مفرط فيتعلق بالتحقق من أكثر مما يحتاجه السلوك، حتى عندما تمر كل التحققات عبر الواجهة العامة.

##أسباب المشكلة

لماذا يحدث ذلك

  • عقلية "المزيد من التحققات = اختبار أكثر شمولاً": يثبت المطورون كل ما يمكنهم رؤيته، خلطاً بين الاكتمال و الصحة.
  • تجعل الأدوات التحديد المفرط هو الطريق الأسهل: حيث تسجل دالة toMatchSnapshot() المخرجات بأكملها، وتجعل أطر المحاكاة من السهل التحقق من كل تفاعل (مثل toHaveBeenCalledWith، وترتيب الاستدعاءات، وعددها).
  • نسخ ولصق كائن حقيقي في toEqual بدلاً من التحقق فقط من الحقل الذي يدور حوله الاختبار.
  • الإعدادات المثقلة بالمحاكاة: حين يصبح كل متعاون كائن محاكاة (المحاكاة المفرطة)، يبدو التحقق من كل واحد منها الطريقة الطبيعية لإنهاء الاختبار.

لماذا يضر ذلك

في كتاب xUnit Test Patterns لجرارد ميسزاروس، هذا هو السبب الجذري وراء رائحة الاختبار الهش (Fragile Test)، المنسوبة إلى البرمجيات المحددة بشكل مفرط (Overspecified Software) و حساسية السلوك (Behavior Sensitivity): حيث تقترن الاختبارات بـ كيفية عمل الكود بدلاً من ما ينتجه، لذا تفشل عند إجراء تغييرات لا تؤثر على السلوك.

  • سهولة الصيانة. تؤدي كل عملية إعادة هيكلة للكود (refactoring) إلى سلسلة متتالية من إخفاقات الاختبارات غير ذات الصلة، مما يجعل الحفاظ على نجاح مجموعة الاختبارات مكلفاً ويمنع بنشاط إعادة هيكلة الكود.
  • الموثوقية / الثقة. الاختبارات التي تطلق "إنذارات كاذبة" على كود صحيح تدرب الفريق على تجاهل الإخفاقات أو إعادة تسجيل اللقطات (snapshots) بشكل أعمى دون التحقق منها.
  • الثقة الزائفة. عادة ما يتزامن التحديد المفرط مع قلة التحقق من النتيجة ذات المغزى: فيمكن للاختبار تثبيت طابع زمني وسطر سجل (log) دون فحص القيمة التي تهم المستخدم بالفعل. وكما توضح أدبيات اختبار التفاعل، فإن التحديد المفرط هو "التحقق من أشياء ليست جزءاً من النتيجة النهائية" — وغالباً ما يكون ذلك عن طريق التحقق من التفاعلات بدلاً من النتائج.
  • سهولة القراءة. جدار من التحققات العارضة يحجب السلوك الوحيد الذي يهدف الاختبار إلى توثيقه.

##المعالجة

تحقق فقط مما يتطلبه السلوك قيد الاختبار — ويفضل التحقق من النتيجة النهائية على التفاعلات الداخلية.

  1. سمِّ النتيجة الوحيدة التي يوثقها الاختبار وتحقق منها، ولا شيء أكثر. قسّم النتائج المختلفة تماماً إلى اختبارات منفصلة مسماة جيداً بدلاً من استخدام تحقق ضخم واحد.
  2. استخدم مطابقات جزئية/مرنة بدلاً من تطابق الكائن بأكمله: مثل expect.objectContaining، أو toMatchObject، أو arrayContaining، أو stringContaining/stringMatching، و expect.any(...) للحقول التي لا يمكنك أو لا ينبغي لك تثبيتها.
  3. قم بتحييد البيانات المتقلبة (الطوابع الزمنية، المعرفات، القيم العشوائية) باستخدام مطابقات الخصائص (مثل expect.any(String)) أو بحقن ساعة/مولد معرفات ثابت — ولا تكتب قيمة اليوم يدوياً بشكل ثابت.
  4. ألغِ افتراضات الترتيب للبيانات غير المرتبة: تحقق من الانتماء للمجموعة (arrayContaining) أو قم بالفرز قبل المقارنة.
  5. فضل التحقق من الحالة على التحقق من السلوك. استبدل الاستعلامات (queries) بـ stubs (ولا تتحقق منها); وتحقق فقط من استدعاء المتعاون عندما يكون هذا الاستدعاء هو التأثير الجانبي الملاحظ (أمر command)، وتجنب التحقق من الأعداد الدقيقة/الترتيب للمتعاونين الداخليين البحتين.
  6. اجعل اللقطات (snapshots) صغيرة ومقصودة، أو استبدل اللقطة المترامية الأطراف ببضعة تحققات موجّهة إلى الحقائق التي يُفترض أن يحميها الاختبار. (أما الوصول إلى DOM عبر container أو الاستعلام عن الحالة الخاصة فمشكلة أخرى، انظر اختبار تفاصيل التنفيذ.)

قبل ← بعد:

// قبل: يقرن الاختبار بالحقول المتقلبة وتسلسل الاستدعاءات الداخلية
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' });
// المعرف وتاريخ الإنشاء عارضان؛ التسجيل وترتيب الاستدعاءات هما تفاصيل تنفيذية — لا تثبتهما

ليس toHaveBeenCalledBefore مطابِقًا أساسيًا في Jest: فهو يأتي من مكتبة jest-extended (ويتضمنه Vitest مدمجًا منذ الإصدار 3.0). وأيًّا كان مشغّل الاختبارات، لا تثبّت ترتيب الاستدعاءات إلا إذا كان الترتيب نفسه هو المتطلب.

##أدوات الكشف

  • eslint-jest jest/no-large-snapshots — يمنع اللقطات الكبيرة؛ فاللقطة الضخمة تثبّت كل تفصيل عرضي في المخرجات
  • eslint-vitest vitest/no-large-snapshots — يمنع اللقطات الكبيرة؛ فاللقطة الضخمة تثبّت كل تفصيل عرضي في المخرجات