الاختبار المسهب.
اختبار يستخدم كوداً أكثر بكثير مما يحتاجه لتوضيح السيناريو الخاص به، مما يدفن علاقة السبب والنتيجة التي يتحقق منها تحت أكواد الإعداد الجاهزة (boilerplate)، والبيانات غير ذات الصلة، والتحققات لكل حقل على حدة.
##العلامات والأعراض
ليس الاختبار المسهب مشكلة مستقلة في كتاب ميسزاروس xUnit Test Patterns، بل يرد فيه اسمًا بديلًا لـالاختبار الغامض. وتركّز هذه الصفحة على عائلة واحدة من أسبابه، وهي الحجم: بناء الكائنات داخل الاختبار، والمعلومات غير ذات الصلة، والتحققات حقلًا بحقل. أما الأسباب الأخرى (الضيف الغامض، والاختبار المتلهف، والتجهيز العام…) فابدأ فيها من مقال الاختبار الغامض.
لا يمكنك معرفة ما يثبته الاختبار بمجرد النظر، على الرغم من عدم وجود أي تعقيد — فالهدف يغرق في حجم الكود. العلامات الشائعة:
- طريقة الاختبار طويلة (يستخدم دليل مصطلحات مشاكل الاختبارات حداً تقريبياً يبلغ أكثر من 30 سطراً)، أو تتجاوز شاشة العرض الواحدة.
- كتلة
arrange(الترتيب) كبيرة تقوم ببناء الكائنات حقلاً بحقل، والكثير منها غير ذي صلة بالسلوك الخاضع للاختبار. - الكثير من القيم الحرفية المكتوبة يدوياً (hard-coded literals) دون وجود رابط واضح بين المدخلات والمخرجات التي يتم التحقق منها.
- يتم فحص النتيجة باستخدام تراكم من التحققات الفردية للحقول بدلاً من مقارنة واحدة ذات مغزى.
- عند قراءة التحققات، لا يمكنك بسهولة ربط المدخلات التي تسببت في القيمة المتوقعة المقابلة.
test('places an order for a returning customer', () => {
const address = new Address();
address.street = '123 Main St';
address.city = 'Springfield';
address.zip = '00000'; // غير ذي صلة بهذا الاختبار
address.country = 'US';
const customer = new Customer();
customer.id = 42;
customer.firstName = 'Ada';
customer.lastName = 'Lovelace';
customer.email = '[email protected]';
customer.address = address;
customer.createdAt = new Date('2020-01-01'); // ضوضاء (تشويش)
const product = new Product();
product.sku = 'SKU-1';
product.name = 'Widget';
product.price = 9.99;
const cart = new Cart();
cart.add(product, 2);
const order = orderService.placeOrder(customer, cart);
expect(order.status).toBe('CONFIRMED'); // المهم فقط: الحالة والمجموع
expect(order.lineItems.length).toBe(1);
expect(order.lineItems[0].sku).toBe('SKU-1');
expect(order.lineItems[0].quantity).toBe(2);
expect(order.total).toBe(19.98);
expect(order.customerId).toBe(42);
});
ثمانية عشر سطرًا لبناء الكائنات وستة تحققات حقلًا بحقل — أي 32 سطرًا في المجموع — تحيط بسيناريو يتلخص في: "قطعتان بسعر 9.99 تنتجان طلبًا مؤكدًا بمجموع 19.98".
##أسباب المشكلة
لماذا يحدث ذلك
- البناء المضمن (Inline) مع الحقول المطلوبة. يجبرك بناء الكائنات الحقيقية من خلال دوال البناء/التعيين (constructors/setters) على تقديم بيانات لا يهتم بها الاختبار، فقط لجعل الكود يترجم ويعمل. ويلاحظ "ميسزاروس" أن مستوى التفاصيل المطلوب لجعل الاختبار قابلاً للتنفيذ قد يجعله "مسهباً لدرجة يصعب معها فهمه".
- نمو النسخ واللصق. يبدأ الاختبار صغيراً، ثم يتم إنشاء كل سيناريو جديد بنسخ السيناريو السابق وتعديل قيمة ما، مما يؤدي إلى تراكم الضوضاء (noise).
- عدم وجود دوال مساعدة مشتركة للبناء. بدون وجود دوال إنشاء (Creation Methods)، أو بناة (builders)، أو هياكل (fixtures)، يعيد كل اختبار تحديد بنية الكائن بالكامل.
- البيانات الثابتة (Hard-coded) والتحقق حقل بحقل. تبدو كتابة القيم الحرفية في كل مكان والتحقق من كل خاصية أمراً "شاملاً"، ولكنه يضاعف عدد السطور.
لماذا يضر ذلك
- المقروئية. كأي اختبار غامض، يخفي علاقة السبب والنتيجة بين التجهيز والنتيجة، وهي الغرض الأساسي من الاختبار القائم على الأمثلة.
- قابلية الصيانة. يميل الاختبار المكون من أكثر من 30 سطراً إلى حمل "مسؤوليات متعددة"، وبالتالي فإن أي تغيير صغير في كود الإنتاج يمتد تأثيره إلى سطور عديدة عبر العديد من الاختبارات. كما أن عمليات الإعداد المضمنة الضخمة تولد التكرار.
- الثقة الزائفة. تخفي الاختبارات الطويلة والصاخبة الأخطاء البرمجية في وضح النهار: فمن السهل تفويت قيمة حرفية غير صحيحة أو تحقق في غير مكانه، ويدفع الإسهاب المراجعين إلى قراءة الكود بشكل سريع وسطي. يبدو حجم الكود صارماً ولكنه لا يثبت شيئاً إضافياً.
- الموقوفية. كلما قام الاختبار بتثبيت حالات غير ذات صلة، زاد احتمال تعطله لأسباب لا علاقة لها بغرضه الأساسي (تحديد مفرط للمواصفات over-specification)، مما ينتج عنه إخفاقات هشة وضعيفة الدلالة.
##المعالجة
ادفع الضوضاء خارج جسم الاختبار بحيث تظل فقط المتغيرات التي تقود السلوك مرئية. خطوات عملية (كلها مأخوذة من أنماط اختبار xUnit):
- استخلص دوال إنشاء (Creation Methods) / استخدم بانياً (builder). استبدل بناء الكائنات المضمن بمساعد ذي اسم معبر يوفر قيماً افتراضية منطقية؛ ولا تمرر سوى القيم التي يهتم بها الاختبار فعلياً (دالة إنشاء محددة بالمعاملات Parameterized Creation Method). يقضي هذا على كل من الأكواد الجاهزة والمعلومات غير ذات الصلة.
- ضع قيمًا افتراضية للبيانات غير الهامة داخل الدالة المساعدة، مما يشير إلى أن "هذه القيم لا تؤثر على النتيجة".
- استبدل الفحوصات حقل بحقل بكائن متوقع (Expected Object) (باستخدام
toEqual/toMatchObject) أو بـ تحقق مخصص (Custom Assertion) / دالة تحقق تسمي المفهوم الجاري التحقق منه. - تحقق من سلوك واحد لكل اختبار. إذا كان الاختبار الواحد طويلاً لأنه يغطي نتائج متعددة (الاختبار المتلهف)، فقم بتقسيمه.
- حول الاختبارات المسهبة شبه المتطابقة إلى اختبارات محددة بالمعاملات (Parameterize) باستخدام
test.each/it.eachبدلاً من النسخ واللصق.
// قبل — انظر العلامات والأعراض (18 سطرًا من الإعداد + 6 تحققات، و32 سطرًا في المجموع)
// بعد
test('confirms the order and charges line-item total', () => {
const customer = aCustomer(); // دالة إنشاء، مع قيم افتراضية
const cart = aCartWith(aProduct({ price: 9.99 }), 2); // المدخلات ذات الصلة فقط
const order = orderService.placeOrder(customer, cart);
expect(order).toMatchObject({ // الكائن المتوقع، وليس حقلاً بحقل
status: 'CONFIRMED',
total: 19.98,
});
});
الآن يمكن قراءة السيناريو في ثوانٍ معدودة: يشتري العميل منتجين بسعر 9.99 دولاراً لكل منهما ← يتم تأكيد الطلب بمجموع 19.98 دولاراً. الدوال المساعدة (مثل aCustomer، وaProduct، وaCartWith) هي أدوات مشتركة ومختبرة، لذا تعيش الضوضاء في مكان واحد بدلاً من تكرارها في كل اختبار.
شبكة حماية: ضع حداً أقصى لطول دالة الاختبار وعدد التحققات في أداة التحليل الإملائي (linter) (انظر أدوات الكشف) حتى لا تنمو الاختبارات بصمت وتتحول مرة أخرى إلى اختبارات مسهبة.
##أدوات الكشف
- eslint max-lines-per-function — فرض حد أقصى لعدد السطور لكل دالة — يبلغ عن أجسام الاختبارات التي تتجاوز الحد المحدد، وهو المقياس الأساسي للاختبار المسهب (Verbose Test).
- eslint max-statements — فرض حد أقصى لعدد العبارات البرمجية (statements) في كتلة الدالة — يضع حداً لما يمكن لاختبار واحد القيام به.
- sonar javascript:S138 — لا ينبغي أن تحتوي الدوال على سطور برمجية كثيرة جداً — ينطبق على دوال الاختبار؛ ويبلغ عن طرق الاختبار ضخمة الحجم ومتعددة المسؤوليات (RSPEC-138).
- eslint-jest jest/max-expects — فرض حد أقصى لعدد استدعاءات expect() لكل اختبار (الافتراضي 5) — يكتشف تراكم التحققات في الاختبارات المسهبة/المتحمسة (eager).
- eslint-vitest vitest/max-expects — فرض حد أقصى لعدد استدعاءات expect() لكل اختبار (الافتراضي 5) — مكافئ Vitest لقاعدة jest/max-expects.