الاختبار المتلهف.
يتحقق الاختبار المتلهف من عدة طرق أو سلوكيات متميزة للوحدة الخاضعة للاختبار في طريقة اختبار واحدة، بدلاً من التركيز على سلوك واحد.
##Signs and Symptoms
تقوم طريقة اختبار واحدة بتشغيل العديد من طرق الإنتاج غير المترابطة والتحقق من كل منها، وتوجيه الكائن عبر سلسلة كاملة من العمليات بدلاً من التحقق من نتيجة واحدة.
العلامات الدالة:
- اسم اختبار غامض وشامل (مثل
testUserService، أوit('works')) أو اسم مركب باستخدام "و" (creates_and_renames_and_deletes). - دورات تنفيذ → تحقق (act → assert) متعددة في جسم اختبار واحد: تستدعي طريقة، وتتحقق، ثم تستدعي أخرى، وتتحقق مجدداً.
- استخدام التعليقات كعناوين للأقسام داخل الاختبار (مثل
// now test delete) لفصل الأشياء التي يتم فحصها. - تلمس التحققات طرقاً أو حقولاً مختلفة للنظام تحت الاختبار (SUT) لا تشترك في نتيجة منطقية واحدة.
// رائحة: اختبار واحد يتحقق من الإنشاء، وإعادة التسمية، والحذف، وسرد القائمة
test('user service', () => {
const svc = new UserService();
const user = svc.create({ name: 'Ada' }); // السلوك 1
expect(user.id).toBeDefined();
svc.rename(user.id, 'Grace'); // السلوك 2
expect(svc.get(user.id).name).toBe('Grace');
svc.delete(user.id); // السلوك 3
expect(svc.get(user.id)).toBeUndefined();
expect(svc.list()).toHaveLength(0); // السلوك 4
});
لاحظ الفرق: يتعلق الاختبار المتلهف باختبار سلوكيات متعددة، وليس مجرد وجود استدعاءات expect متعددة. إن إجراء عدة تحققات على نتيجة منطقية واحدة (مثل فحص عدة حقول لنفس الكائن نفسه المرجّع) أمر مقبول ولا يعد هذه الرائحة.
##Reasons for the Problem
لماذا يحدث ذلك
- إعادة استخدام التجهيزات / الكسل. تجهيز الترتيبات (arrange) قد يكون مملاً أو مكلفاً، لذا من المغري الاستمرار في التعامل مع الكائن الذي تم إنشاؤه بالفعل والتحقق من صحته "بما أننا هنا".
- التفكير بأسلوب سير العمل. يختبر كاتب الكود رحلة المستخدم ("إنشاء، ثم تعديل، ثم حذف") كسيناريو واحد بدلاً من عزل كل سلوك للوحدة.
- الانحراف عن التطوير الموجه بالاختبار (TDD). يتراكم في الاختبار الذي بدأ مركزاً تحققات إضافية مع إضافة وظائف جديدة بدلاً من تخصيص اختبار مستقل لها.
لماذا يضر ذلك
- الثقة الزائفة (المشكلة الكبرى). معظم مكتبات التحقق توقف الاختبار عند أول تحقق يفشل. فإذا تعطلت عملية الإنشاء (
create)، فلن يتم تشغيل فحوصات إعادة التسمية (rename) والحذف (delete) وسرد القائمة (list) أبداً — وبالتالي فإن التحول من اللون الأخضر إلى الأحمر يخفي عدد السلوكيات المعطلة بالفعل، ولم يكن الاختبار الناجح يختبر الخطوات اللاحقة فعلياً بمجرد تراجع خطوة سابقة. - ضعف التشخيص. يخبرك الفشل أن "اختبار خدمة المستخدم قد فشل"، وليس أي سلوك بالتحديد. يتعين عليك قراءة الطريقة بأكملها لتحديد الخطوة المعطلة.
- حجب الغرض / صعوبة القراءة. لم يعد الاختبار يوثق حقيقة واحدة عن النظام؛ بل يجب على القارئ تقسيمه ذهنياً إلى السلوكيات التي يجمعها.
- الهشاشة والاقتران. تعتمد التحققات اللاحقة على التعديلات السابقة، لذا فإن تغييراً غير ذي صلة بالقرب من الأعلى يتسلسل ويجعل الاختبار هشاً وصعب التعديل (refactor).
- سهولة الصيانة. من الصعب حذف أو نقل أو إعادة تسمية تغطية سلوك ما عندما تكون متشابكة مع ثلاثة سلوكيات أخرى في طريقة واحدة.
##Treatment
قسّم الاختبار المتلهف إلى عدة اختبارات مركزة — سلوك واحد لكل اختبار — وارفع خطوة الترتيب (arrange) المشتركة إلى التهيئة (setup) حتى لا يكرر هذا التقسيم الكود المكرر.
الخطوات:
- حدد السلوكيات التي يجمعها الاختبار (هنا: الإنشاء، وإعادة التسمية، والحذف، وسرد القائمة بعد الحذف).
- استخرج اختباراً واحداً لكل سلوك، يحمل كل منها اسماً وصفياً يوضح الغرض منه.
- انقل التهيئة المشتركة إلى
beforeEachأو طريقة مصنع/إنشاء (Creation Method) بحيث يظل لكل اختبار نظام تحت اختبار (SUT) نظيف دون تكرار كود الترتيب. - احتفظ بتنفيذ (Act) واحد لكل اختبار وتحقق فقط من نتيجة هذا السلوك (إجراء تحققات متعددة باستخدام
expectعلى نفس النتيجة أمر مقبول). - إذا كنت بحاجة فعلياً إلى التحقق من صحة سير عمل (workflow) كامل من البداية إلى النهاية، فاحتفظ به كسيناريو أو اختبار تكامل مسمى بوضوح — ولكن مع ذلك قم بتغطية كل سلوك وحدة في اختبار خاص به بدلاً من الاعتماد على سير العمل للتغطية.
- احمِ الكود من التراجع عن طريق تفعيل قاعدة الحد الأقصى للتحققات (انظر كواشف الرائحة) كمؤشر بسيط.
// بعد: اختبارات مركزة، تهيئة مشتركة
let svc;
beforeEach(() => { svc = new UserService(); });
test('create() assigns an id', () => {
expect(svc.create({ name: 'Ada' }).id).toBeDefined();
});
test('rename() updates the stored name', () => {
const { id } = svc.create({ name: 'Ada' });
svc.rename(id, 'Grace');
expect(svc.get(id).name).toBe('Grace');
});
test('delete() removes the user', () => {
const { id } = svc.create({ name: 'Ada' });
svc.delete(id);
expect(svc.get(id)).toBeUndefined();
});
الآن يمكن لأي سلوك فردي أن يفشل بشكل مستقل، ويحدد اسم الاختبار الفاشل مكان العطل بدقة، ويُقرأ كل اختبار كحقيقة موثقة عن النظام تحت الاختبار (SUT).
##Detected by
- eslint-jest jest/max-expects — فرض حد أقصى لعدد استدعاءات expect() لكل اختبار
- eslint-vitest vitest/max-expects — فرض حد أقصى لعدد expect لكل اختبار