ConstructiCat Logo
CodeBust.
Browse section ▾

التجهيز العام.

تقوم عملية تهيئة مشتركة ببناء تجهيز واحد ضخم وشامل يغطي احتياجات كل الاختبارات، ولكن كل اختبار فردي يشغل جزءاً صغيراً جداً منه فقط.

##Signs and Symptoms

تقوم كتلة beforeEach/setUp واحدة (أو تهيئة مشتركة على مستوى الوحدة) بإنشاء الكثير من الكائنات، وتغذية العديد من السجلات، وربط عدة محاكيات — بينما يلمس أي اختبار معين واحداً أو اثنين منها فقط. المبدأ الموجه للاكتشاف من كتالوج روائح الاختبار هو ببساطة: لا تُستخدم جميع الحقول التي تم إنشاؤها في التهيئة بواسطة جميع طرق الاختبار.

كيف تتعرف عليه:

  • كتلة التهيئة طويلة وتنمو في كل مرة يتم فيها إضافة اختبار جديد.
  • لفهم اختبار واحد، يجب عليك التمرير للأعلى وهندسة عكسية لمعرفة أي أجزاء من التجهيز تهمه فعلياً.
  • تفشل العديد من الاختبارات أو تحتاج إلى تعديل عندما تغير كائناً مشتركاً واحداً لا يهتم به معظمها حتى.
  • يخدم نفس التجهيز سيناريوهات مختلفة تماماً (الإنشاء، التحقق من الصحة، الدفع، الصلاحيات) من مكان واحد.
// رائحة: تجهيز واحد لكل شيء؛ ويستخدم كل اختبار جزءاً ضغيراً منه
let user, admin, product, cart, order, paymentGateway, shippingZone;

beforeEach(() => {
  user            = createUser({ name: 'Ada' });
  admin           = createAdmin();
  product         = createProduct({ price: 10 });
  cart            = createCart(user, [product]);
  order           = createOrder(cart);
  paymentGateway  = mockGateway();
  shippingZone    = createZone('EU');
});

test('cart subtotal sums its line items', () => {
  // يحتاج فقط إلى السلة + المنتج، ومع ذلك يدفع تكلفة المسؤول والطلب والبوابة والمنطقة...
  expect(cart.subtotal()).toBe(10);
});

##Reasons for the Problem

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

  • المبالغة في تطبيق مبدأ DRY (عدم التكرار). يتم دمج التهيئة في خطاف مشترك واحد لـ "تجنب التكرار"، وبذلك تُضاف احتياجات كل اختبار جديد إلى نفس التجهيز.
  • النمو العضوي. يتراكم في التجهيز كائنات جديدة مع إضافة اختبارات جديدة، ولا يقوم أحد بتنظيف ما لم تعد الاختبارات الأقدم تستخدمه.
  • عطالة التهيئة الضمنية. بمجرد وجود كتلة beforeEach كبيرة، تصبح إضافة سطر آخر أسهل من إنشاء تجهيز مركز للحالة الجديدة.

لماذا يضر ذلك

  • سهولة القراءة / الاختبارات كتوثيق. يجب أن يُقرأ الاختبار كعلاقة واضحة بين السبب والنتيجة (cause→effect). عندما يكون معظم التجهيز عبارة عن ضوضاء غير ذات صلة، لا يستطيع القارئ تحديد ما الذي يؤدي فعلياً إلى النتيجة. ترياق ميسزاروس، وهو التجهيز الأدنى (Minimal Fixture)، موجود على وجه التحديد لأن الاختبار الذي يستخدم أصغر تجهيز ممكن يكون دائماً أسهل في الفهم.
  • سهولة الصيانة. التغيير الذي يتطلبه اختبار واحد (مثل إعطاء product حقلاً مطلوباً جديداً) يفرض تعديل التهيئة المشتركة بين جميع الاختبارات، مما يخلق تأثيراً متسلسلاً من الأعطال ويقرن الاختبارات غير المترابطة ببعضها.
  • الموثوقية. تتيح حالة التجهيز المشتركة والقابلة للتعديل للاختبارات التأثير على بعضها البعض وتجعل تحديد مكان الأعطال أمراً صعباً — وهو مصدر كلاسيكي لعدم الاستقرار المرتبط بالترتيب.
  • السرعة. يدفع كل اختبار التكلفة الكاملة لبناء التجهيز بأكمله. يتم إدراج التجهيز العام ضمن أسباب بطء الاختبارات.
  • الثقة الزائفة. تصبح الاختبارات محددة بشكل مفرط (over-specified) بناءً على تفاصيل تجهيز عارضة، لذا تنجح أو تفشل لأسباب لا علاقة لها بالسلوك قيد الاختبار.

##Treatment

احرص على استخدام تجهيز أدنى (Minimal Fixture): يجهز كل اختبار ما يحتاجه فقط، ولا شيء أكثر.

  1. جرد الاستخدام. لكل حقل مشترك، حدد الاختبارات التي تقرأه بالفعل. وأي حقل تستخدمه مجموعة فرعية فقط هو مرشح للنقل خارج التهيئة المشتركة.
  2. ادفع البناء إلى طرق الإنشاء / بناة بيانات الاختبار (التهيئة المفوضة Delegated Setup). حافظ على إيجاز الاختبارات دون فرض تجهيز واحد عملاق — يستدعي كل اختبار بناءً (builder) ويتجاوز فقط الخصائص ذات الصلة به.
  3. فضل استخدام تجهيز جديد (Fresh Fixture) لكل اختبار على استخدام تجهيز مشترك طويل الأمد، مما يلغي الاقتران بين الاختبارات والحالة المشتركة القابلة للتعديل.
  4. التقسيم حسب السيناريو. إذا كانت مجموعات من الاختبارات تشترك فعلياً في تجهيز صغير، فافصلها إلى كتل describe مركزة (أو فئات اختبار)، لكل منها تهيئتها الصغرى الخاصة بها.
  5. احتفظ في beforeEach بما هو مشترك وصغير حقاً فقط; وانقل الباقي إلى داخل الاختبار (inline) ليكون السبب والنتيجة متجاورين مع التحقق.
// قبل: تجهيز عام في خطاف مشترك (انظر العلامات والأعراض)

// بعد: تهيئة صغرى ومفوضة — يبني كل اختبار فقط ما يحتاجه
test('cart subtotal sums its line items', () => {
  const cart = aCart().withItem(aProduct().price(10)).build();
  expect(cart.subtotal()).toBe(10);
});

test('checkout charges the payment gateway', () => {
  const gateway = mockGateway();
  const cart    = aCart().withItem(aProduct().price(10)).build();

  checkout(cart, gateway);

  expect(gateway.charge).toHaveBeenCalledWith(10);
});

يُقرأ كل اختبار الآن من الأعلى إلى الأسفل كقصة قائمة بذاتها، ويمتص البناة (builders) الكود المكرر، ولم يعد تغيير سيناريو واحد يزعج الآخرين.