ConstructiCat Logo
CodeBust.
Browse section ▾

الاختبار الغامض.

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

##Signs and Symptoms

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

  • الاختبار المتلهف (Eager Test) — طريقة اختبار واحدة تتحقق من سلوكيات متعددة غير مترابطة، لذا لا يوجد هدف واحد واضح.
  • الضيف الغامض (Mystery Guest) — يعيش التجهيز أو القيم المتوقعة خارج الاختبار (ملف، أو سجل قاعدة بيانات مشترك، أو تجهيز يتم تحميله بالاسم)، لذا لا يمكنك معرفة سبب نجاح التحقق.
  • التجهيز العام (General Fixture) — يقوم خطاف beforeEach/مصنع مشترك كبير ببناء ما هو أكثر بكثير مما يحتاجه هذا الاختبار; وتضيع الحقول القليلة ذات الصلة في الضوضاء.
  • المعلومات غير ذات الصلة (Irrelevant Information) — صفحات من بيانات التهيئة أو لقطة شاشة (snapshot) عملاقة حيث لا يهم سوى حقل أو حقلين فقط.
  • بيانات اختبار ثابتة ومكتوبة يدوياً (Hard-Coded Test Data) — قيم ثابتة سحرية (مثل 42، أو "a3f9-..."، أو userId=7) دون معنى مسمى، وتتكرر عبر التهيئة والتحققات.
  • الاختبار غير المباشر (Indirect Testing) — يقوم الاختبار بنكز النظام تحت الاختبار من خلال عدة كائنات أخرى، مما يحجب ما هو قيد الاختبار بالفعل.
// غامض: متلهف + ضيف غامض + معلومات غير ذات صلة
test('user', async () => {
  const data = loadFixture('users.json');        // ضيف غامض: القيم تعيش في ملف
  const svc = new UserService(data, cfg, clock); // تجهيز عام: معظم هذا غير مستخدم
  const u = await svc.create({ ...data[0], roles: ['a','b'], flags: 0x1f });

  expect(u.id).toBeDefined();
  expect(u.email).toContain('@');
  expect(await svc.count()).toBe(data.length + 1); // لماذا هذا الرقم؟ الإجابة موجودة في الملف
  expect(await svc.login(u.email, 'p@ss')).toBe(true); // سلوك ثانٍ غير ذي صلة
});

هنا لا يمكنك معرفة ما يثبته الاختبار دون فتح ملف users.json، وهو يتحقق فعلياً من الإنشاء و تسجيل الدخول في وقت واحد.

##Reasons for the Problem

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

  • تنمو الاختبارات بالتراكم: يضيف المطور "تحققاً إضافياً" إلى اختبار موجود بدلاً من كتابة اختبار جديد، مما ينتج عنه اختبار متلهف.
  • تبدو مشاركة التهيئة فعالة، لذا يُعاد استخدام تجهيز واحد واسع النطاق أو beforeEach في كل مكان (التجهيز العام)، ويتم تحميل ملفات خارجية/بذور قاعدة بيانات بالاسم (الضيف الغامض).
  • يؤدي نسخ ولصق بيانات تبدو واقعية إلى سحب العشرات من الحقول غير ذات الصلة والأرقام السحرية.
  • المحاكاة المفرطة أو المرور عبر العديد من المتعاونين يحول اختبار الوحدة إلى اختبار غير مباشر (Indirect Testing).

لماذا يضر ذلك

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

##Treatment

اجعل كل اختبار يروي قصة قائمة بذاتها يكون غرضها واضحاً في جسم الاختبار.

  1. تحقق من شرط واحد لكل اختبار. قسّم الاختبار المتلهف إلى اختبارات مركزة، يحمل كل منها اسماً يوضح السلوك الذي يفحصه. هذا يعالج أيضاً مشكلة حجب التغطية.
  2. أدرج التجهيز ذي الصلة في نفس السطر (Inline) (اقضِ على الضيف الغامض). ابنِ البيانات التي يعتمد عليها الاختبار داخل الاختبار نفسه، أو عبر طريقة إنشاء/بناء صريحة ومسماة — ولا تقم بتحميل ملفات مجهولة أو صفوف مشتركة من قاعدة البيانات.
  3. استخدم تجهيزاً أدنى / جديداً (Minimal / Fresh Fixture). ابنِ فقط ما يحتاجه هذا الاختبار; واستبدل كتلة beforeEach المشتركة الكبيرة ببناء (builder) يضع قيماً افتراضية للضوضاء ويسمح لكل اختبار بتعيين الحقل قيد الاختبار فقط.
  4. سمِّ بياناتك. استبدل القيم الثابتة السحرية بثوابت/متغيرات توضح النية بحيث يفسر التحقق نفسه.
  5. تحقق من المعنى، وليس من كل شيء. فضل التحققات المحددة (targeted assertions) على استخدام لقطات الشاشة (snapshots) العملاقة; وإذا استخدمت اللقطات، فاجعلها صغيرة وقابلة للمراجعة حتى لا تغرق الحقائق المهمة في مخرجات غير مهمة.
  6. اخفِ التفاصيل الميكانيكية، وليس الغرض. ادفع الربط العارض إلى طرق مساعدة/أدوات اختبار مسمية جيداً بحيث يُقرأ جسم الاختبار كتهيئة ← تنفيذ ← توقع.
// قبل (غامض)
test('user', async () => {
  const data = loadFixture('users.json');
  const svc = new UserService(data, cfg, clock);
  const u = await svc.create({ ...data[0], roles: ['a','b'], flags: 0x1f });
  expect(await svc.count()).toBe(data.length + 1);
});

// بعد: غرض واحد، تجهيز مضمن، بيانات مسماة
test('create() persists a new user', async () => {
  const svc = userServiceWith([]);                 // تجهيز أدنى وجديد
  const newUser = aUser({ email: 'ada@example.com' }); // يضع البناء (builder) قيماً افتراضية للضوضاء

  const created = await svc.create(newUser);

  expect(created.email).toBe('ada@example.com');
  expect(await svc.count()).toBe(1);               // يفسر نفسه بنفسه، لا يوجد ملف خارجي
});

احرص على اتباع هيكل AAA (الترتيب والتنفيذ والتحقق): يجب أن يستوعب القارئ السيناريو دون مغادرة طريقة الاختبار.

##Detected by