ConstructiCat Logo
CodeBust.
Browse section ▾

الضيف الغامض.

اختبار تعيش مدخلاته أو نتائجه المتوقعة في مورد خارجي — ملف، أو بذرة قاعدة بيانات (seed)، أو تجهيز مشترك — بحيث لا يمكنك فهم الاختبار أو الثقة به بمجرد قراءته بمفرده.

##Signs and Symptoms

لا يمكنك فهم الاختبار بمجرد قراءته: فالبيانات التي تحركه (وتبرر تحققاته) تعيش في مكان ما خارج الشاشة — ملف CSV/JSON، أو سيناريو بذرة قاعدة البيانات (seed)، أو وحدة تجهيز مشتركة، أو تعليق توضيحي لتجهيز بيانات إطار العمل. يشير الاختبار إلى مورد غامض ثم يتحقق من قيم "سحرية" يكون معناها مخفياً في هذا المورد.

العلامات الدالة:

  • يستدعي جسم الاختبار شيئاً مثل readFileSync('fixtures/subscribers.csv')، أو loadSeed('invoices.sql')، أو getResource(...)، أو استدعاء beforeAll عام يغذي قاعدة بيانات مشتركة بالبيانات.
  • تبدو القيم المتوقعة عشوائية (مثل toHaveLength(3)، أو معرف/بريد إلكتروني محدد) ويجب عليك فتح ملف آخر لمعرفة السبب.
  • إعادة استخدام تجهيز مشترك/"عام" عبر العديد من الاختبارات، بحيث تكون المدخلات ذات الصلة بـ هذا الاختبار مدفونة بين بيانات لا يهتم بها.
  • تنكسر الاختبارات عندما يقوم زميل بتعديل تجهيز مشترك من أجل اختبار آخر غير ذي صلة.
test('returns active subscribers', async () => {
  // الضيف الغامض: ما هي الصفوف الموجودة في هذا الملف؟ ولماذا الرقم 3 صحيح؟
  const subscribers = await loadSubscribersFromCsv('./fixtures/subscribers.csv');
  const result = filterActive(subscribers);
  expect(result).toHaveLength(3); // التبرير يعيش خارج الاختبار
});

المثال الأصلي من ميسزاروس/كتالوج روائح الاختبار له نفس الشكل: loadAirportsAndFlightsFromFile("test-flights.csv") متبوعاً بـ assertEquals(1, flightsAtOrigin.size()) — لا يكون الرقم "1" منطقياً إلا إذا قرأت ملف test-flights.csv.

##Reasons for the Problem

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

  • المبالغة في تطبيق مبدأ DRY (عدم التكرار). يتم استخراج بيانات الاختبار إلى ملف مشترك أو "تجهيز عام" لتجنب التكرار، مما يضحي بسهولة القراءة في سبيل إعادة الاستخدام.
  • السهولة مع بيانات واقعية. تصدير ملفات CSV/JSON حقيقية أو ملف seed.sql يبدو أسهل من بناء الكائنات في نفس الكود (inline).
  • تهيئة الأنظمة القديمة/التكامل. ترث مجموعات الاختبارات التي تشغل قاعدة بيانات مشتركة أو تعتمد على تعليقات توضيحية لتجهيز البيانات في إطار العمل (مثل @magentoDataFixture …) حالة مخفية افتراضياً.

لماذا يضر ذلك

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

##Treatment

اجعل المدخلات ذات الصلة مرئية داخل الاختبار، بجانب التحقق الذي يعتمد عليها (تجهيز جديد Fresh Fixture / تهيئة مضمنة inline). الهدف هو أن تصبح القيمة المتوقعة بديهية.

خطوات ملموسة:

  1. أدرج البيانات المهمة في نفس السطر (Inline). ابنِ الكائنات/الصفوف القليلة التي يكترث لها الاختبار داخل جسم الاختبار، بحيث تكون القيمة المتوقعة للتحقق صحيحة بشكل واضح.
  2. إذا كنت بحاجة فعلياً لملف، فابنه داخل الاختبار. استخدم دالة مساعدة تأخذ فقط المعلمات البارزة وتكتب في مسار مؤقت، ثم قم بالتنظيف — حتى تظهر القيم ذات المغزى في الاختبار، وليس في ملف مخزن في المستودع.
  3. استبدل التجهيزات المشتركة/"العامة" بتجهيزات مخصصة لكل اختبار، أو اعرض دوال إنشاء/بحث توضح النية (مثل createProductWithName('Simple Product')، أو getRecentlyAddedProduct()) بحيث تكون الخصائص قيد الاختبار واضحة بينما تظل التهيئة غير ذات الصلة مخفية خلف بناء (builder) مسمى جيداً — وليس خلف مورد غامض.

قبل ← بعد:

// قبل — الضيف الغامض: البيانات والرقم "2" مخفيان في ملف البذرة (seed)
test('flags overdue invoices', async () => {
  await seedDatabaseFromFixture('invoices.sql');
  const overdue = await findOverdueInvoices();
  expect(overdue).toHaveLength(2);
});

// بعد — المدخلات مرئية؛ النتيجة المتوقعة واضحة
test('flags overdue invoices', async () => {
  await insertInvoice({ id: 1, dueDate: '2020-01-01', paid: false }); // متأخر
  await insertInvoice({ id: 2, dueDate: '2099-01-01', paid: false }); // لم يحن موعد استحقاقه بعد
  const overdue = await findOverdueInvoices();
  expect(overdue.map(i => i.id)).toEqual([1]);
});

بالنسبة لحالة الملف، فضل استخدام دالة مساعدة مركزة على استخدام ملف مخزن في المستودع:

const csv = makeSubscriberCsv(tmpFile, 'active@x.com', 'active@y.com', 'active@z.com');
// الآن أصبح الرقم "3 نشط" مبرراً بما تراه في الاختبار، وليس بملف مخفي