الاختبارات المُعامَلة والديناميكية
الاختبارات المُعامَلة والديناميكية
كتابة منطق الاختبار ذاته مرات عديدة بمدخلات مختلفة فخٌّ صيانة حقيقي. يحلّ JUnit 5 هذه المشكلة بأناقة عبر @ParameterizedTest، الذي يُشغّل دالة اختبار واحدة مرةً لكل صف من المدخلات. وللحالات التي يجب فيها حساب مجموعة الاختبارات في وقت التشغيل، يُنشئ @TestFactory كائنات DynamicTest ديناميكيًا. يُلغي هذان الأسلوبان معًا الاختبارات المنسوخة ويجعلان تغطية الحالات الحدّية صريحةً ومنهجية.
لماذا تهمّ المُعاملة؟
تأمّل مُتحقِّقًا يجب أن يقبل مجموعة متنوعة من المدخلات الصحيحة ويرفض مجموعة مماثلة من المدخلات الخاطئة. بدون المُعاملة ستكتب اختبارًا لكل حالة — ستة مدخلات تصبح ست دوال شبه متطابقة. بـ @ParameterizedTest تُعبّر عن تلك الحالات الست في جدول بيانات وتحتفظ بجسم تأكيد واحد. الفوائد ملموسة:
- إصلاح خطأ واحد في منطق التأكيد يُصلح كل حالة دفعةً واحدة.
- إضافة حالة حدّية جديدة تعني سطرًا واحدًا في مصدر البيانات، لا دالة جديدة.
- تعرض تقارير الاختبار كل مجموعة وسيطات على حدة، فيتحدّد الفشل فورًا.
@ParameterizedTest في junit-jupiter-params. إن استخدمت مُجمِّع junit-jupiter البومي فهو مُضمَّن تلقائيًا؛ وإلا أضفه صراحةً في ملف البناء.
@ValueSource — وسيطات عددية/نصية مُضمَّنة
@ValueSource هو أبسط مصدر. تُدرج فيه قيمًا حرفية من نوع واحد ويُغذّيها JUnit واحدةً تلو الأخرى إلى دالة الاختبار. الأنواع المدعومة تشمل int وlong وdouble وString وClass وغيرها.
تُخصّص سمة name على @ParameterizedTest اسم العرض. يُستبدل {0} بالوسيط الأول و{displayName} باسم الدالة. الاختبارات ذات الأسماء الجيدة تجعل مخرجات CI مقروءةً دون فتح الكود المصدري.
@MethodSource — وسيطات من دالة مصنع
يستدعي @MethodSource دالةً static تُعيد Stream (أو Iterable أو Iterator أو مصفوفة) من الوسيطات. هذا هو الخيار الصحيح حين تكون الوسيطات كائنات معقدة أو تتطلب بناءً غير تافه.
حين يحمل مصدر الدالة الاسم ذاته لدالة الاختبار يمكنك حذف وسيط السلسلة النصية: يبحث @MethodSource بلا قيمة عن دالة ستاتيكية بالاسم ذاته تلقائيًا. لإعادة الاستخدام عبر الفصول، اذكر المسار الكامل: "com.example.Providers#discountScenarios".
@MethodSource قبل إنشاء نسخة فصل الاختبار. يجب ألا تعتمد على حالة النسخة أو سياق Spring أو دخل/خرج خارجي. إن احتجت وسيطات من قاعدة بيانات، استخدم @MethodSource مع مساعد يقرأ من بنية بيانات في الذاكرة جُهِّزت في @BeforeAll.
@CsvSource و@CsvFileSource — البيانات الجدولية
يُعبّر @CsvSource عن صفوف متعددة الأعمدة كسلاسل نصية محاطة بعلامات اقتباس، إذ يبقى البيانات قريبةً من الاختبار دون حاجة لدالة مصنع منفصلة. يُحوّل JUnit كل عمود إلى نوع المعامل تلقائيًا.
علامات الاقتباس المفردة داخل قيمة CSV تُحدّد سلاسل مُضمَّنة تحتوي على فواصل أو مسافات. القيمة الفارغة تُكتب '' وتُحوَّل إلى String فارغ (ليس null). لمجموعات البيانات الأكبر، انقل البيانات إلى ملف واستخدم @CsvFileSource(resources = "/test-data/truncate.csv") — يقرأه JUnit من classpath الاختبار.
String وEnum والأنواع التي لها مُنشئ بـ String واحد أو دالة ستاتيكية valueOf. للكائنات الخاصة بالنطاق (مثل Money أو UserId) استخدم @MethodSource وأنشئها صراحةً في المصنع. محاولة تحويل كائنات تعسفية عبر CSV يؤدي إلى أخطاء غامضة.
مصادر أخرى: NullAndEmpty وEnumSource
يوفّر JUnit 5 عدة مصادر مدمجة أخرى تستحق المعرفة:
@NullSource/@EmptySource— يُحقنانnullأو قيمة فارغة (سلسلة فارغة، قائمة، مصفوفة). ادمجهما بـ@NullAndEmptySourceللتحقق الدفاعي.@EnumSource— يمرّ على ثوابت Enum، مع خيارnamesوmode(INCLUDE / EXCLUDE / MATCH_ALL / MATCH_ANY) للاختيار الدقيق.
الاختبارات الديناميكية بـ @TestFactory
أحيانًا لا يمكن التعبير عن مجموعة الاختبارات بصورة ستاتيكية — فهي تعتمد على بيانات تُكتشف في وقت التشغيل (مثل ملفات في دليل، أو صفوف من مستودع في الذاكرة، أو مدخلات مُحلَّلة من ملف إعداد). يُعيد @TestFactory Stream<DynamicTest> أو أي Iterable من DynamicNode. كل DynamicTest له اسم عرض وExecutable (لامبدا).
خلافًا لـ @ParameterizedTest، دوال @TestFactory لا تُزيَّن بـ @Test ويمكنها أن تُنتج صفرًا من الاختبارات (قد يكون الـ Stream فارغًا). هذا يجعلها مناسبةً للسيناريوهات القائمة على الاكتشاف حيث "لم يُعثر على عناصر" نتيجةٌ صالحة لا فشل.
@ParameterizedTest أبسط، ودعم IDE له أفضل، ومصادر وسيطاته مرئية في التحكم بالإصدار. احتفظ بـ @TestFactory للحالات الديناميكية الحقيقية — استطلاع نظام الملفات، التحقق من عقود API عبر قائمة نقاط نهاية محمّلة من إعداد، أو الفحوصات الشاملة على فضاء مدخلات محسوب.
أفضل الممارسات المهنية
- سمِّ اختباراتك المُعامَلة. اضبط دائمًا سمة
nameوصفية.name = "[{index}] input={0}"حدٌّ أدنى؛ الأسماء ذات المعنى مثل"price={0}, tier={1}"أفضل. - غطِّ الحدود والمدخلات الخاطئة صراحةً. المُعاملة تُخفّض تكلفة إضافة الحالات — استغل هذا الرخص لاختبار الحدود والمدخلات الفارغة والقيم القصوى والأخطاء التاريخية المعروفة.
- أبقِ كل صف مستقلًا. يجب ألا يعتمد الاختبار المُعامَل على ترتيب التنفيذ. كل استدعاء اختبار منفصل في نموذج JUnit.
- تجنّب الانفجار التوافقي. إن كانت لديك خمسة معاملات بعشر قيم لكل منها، فإن 10^5 تركيبة تُنتج ضوضاءً لا إشارة. اختبر التركيبات ذات المعنى، لا حاصل الضرب الديكارتي.
الخلاصة
يُحوّل @ParameterizedTest مع @ValueSource و@MethodSource و@CsvSource دوال الاختبار المتكررة إلى جداول بيانات أنيقة. يتعامل @TestFactory مع الحالات المتبقية حيث المدخلات مجهولة حتى وقت التشغيل. يتيح لك كلاهما رفع التغطية دون رفع تكلفة الصيانة — وهو الوعد الجوهري لمجموعة الاختبارات الجيدة.