أفضل ممارسات الاختبار
أفضل ممارسات الاختبار
كتابة اختبارات تنجح أمر سهل. أمّا كتابة اختبارات تُعبّر عن القصد وتصمد أمام إعادة الهيكلة وتكشف الأخطاء الحقيقية فهي حرفة بحدّ ذاتها. تستعرض هذه الدرس أهمّ الممارسات الفضلى حول ثلاثة محاور: التسمية، والعزل، والنطاق — ما الذي يجب اختباره وما الذي يجب تركه خارج نطاق الاختبار.
١. سمِّ الاختبارات كمواصفات
اسم الاختبار هو أوّل ما يظهر عند فشل البناء. يجب أن يُجيب على ثلاثة أسئلة دون قراءة الجسم: ما الذي يُختبَر، وفي أيّ حالة، وما المتوقَّع.
النمط الشائع هو Given / When / Then مضغوطًا في اسم الدالة:
الشرطة السفلية داخل الاسم مقبولة (تفضّلها فرق كثيرة) لأنّها تجعل الهيكل الثلاثي قابلًا للمسح البصري في نتائج IDE وتقارير CI. يمكنك بديلًا استخدام @DisplayName لإضافة تسمية بجملة عادية دون تغيير المعرّف:
٢. تحقُّق منطقي واحد في كل اختبار
يجب أن يتحقّق كل اختبار من سلوك واحد بالضبط. هذا لا يعني دائمًا استدعاء assertX() مرّة واحدة فحسب — أحيانًا يستلزم التحقّق من سلوك واحد فحص قيمتين مرتبطتين. لكنّ الاختبار الذي يتحقّق من عشرة أشياء غير مترابطة يُخفي سبب الفشل الجذري.
٣. عزل النظام قيد الاختبار
يجب أن يُنشئ كل اختبار حالته الابتدائية الخاصة النظيفة. الحالة المتغيّرة المشتركة بين الاختبارات تُسبّب فشلًا تبعيًا — اختبارات تنجح منفردةً وتفشل عند تغيير الترتيب. JUnit 5 يُنشئ نسخة جديدة من الصنف لكل دالة اختبار بالافتراض، وهذا مُفيد، لكنّ عليك أيضًا:
- تهيئة البيانات الابتدائية في
@BeforeEachلا في حقول ثابتة. - استبدال المتعاونين الحقيقيين بمحاكيات (mocks) أو بدائل (fakes) كي يختبر الاختبار الصنف المقصود فقط.
- عدم الاعتماد على نظام الملفات أو قاعدة بيانات حيّة أو نقطة HTTP خارجية في اختبار الوحدة.
@AfterEach، فإنّ اختبارًا يرمي استثناءً قبل التنظيف يترك الحالة ملوّثة للاختبار التالي. استخدم حقول نسخ تُهيَّأ في @BeforeEach.
٤. اتّبع هيكل Arrange–Act–Assert (AAA)
يجب أن يحتوي كل اختبار على ثلاث مراحل منفصلة: تهيئة البيانات والمتعاونين (Arrange)، ثمّ استدعاء الكود المُختبَر (Act)، ثمّ التحقّق من النتيجة (Assert). سطر فارغ بين كل مرحلة يجعل البنية مقروءة فورًا.
٥. ما الذي يجب اختباره
ركّز الاختبارات على السلوك القابل للملاحظة من منظور المُستدعي، لا على تفاصيل التنفيذ الداخلية.
- عقود الواجهة العامة — القيم المُعادة، والاستثناءات المُرمَاة، وتغيّرات الحالة المرئية عبر الـ getters.
- جميع مسارات المنطق الشرطي — المسار السعيد، وكل مسار خطأ ذي معنى، والحالات الحدّية (null، فارغ، صفر، أقصى قيمة).
- قواعد العمل — التحقّقات، والحسابات، والثوابت التي يجب أن يلتزم بها نموذج المجال.
- نقاط التكامل عبر اختبارات التكامل — أنّ استعلام
JpaRepositoryيُعيد الصفوف الصحيحة، وأنّ نقطة HTTP تستجيب بالرمز الصحيح.
٦. ما الذي يجب عدم اختباره
الكود غير المُختبَر خطر، لكنّ الإفراط في الاختبار خطر أيضًا: الاختبارات المرتبطة بتفاصيل التنفيذ تنكسر مع كل إعادة هيكلة حتى حين يكون السلوك صحيحًا، ممّا يجعل المطوّرين لا يثقون بالمجموعة.
- الدوال الخاصة (private) — اختبرها عبر الواجهة العامة التي تستدعيها. إن كانت الدالة الخاصة معقّدة لدرجة تستلزم اختبارها مباشرةً، استخرجها إلى صنف متعاون.
- أُطر العمل ذاتها — لا تختبر أنّ Spring يُسلّك Bean أو أنّ JPA يُولّد SQL صحيحًا. ثق بإطار العمل؛ اختبر منطقك أنت.
- الـ getters/setters التافهة — getter يُعيد حقلًا لا يحتوي منطقًا. اختباره يُضيف ضجيجًا دون أمان.
- بيانات الإعداد — ثابت أو قيمة enum أو ملف خصائص لا يحتاج اختبار وحدة.
٧. احرص على سرعة الاختبارات وحتميّتها
مجموعة اختبارات تستغرق عشر دقائق لن تُشغَّل قبل كل commit. احرص على السرعة بلا هوادة:
- استبدل التبعيات البطيئة (قواعد البيانات، قوائم الانتظار، HTTP) بتطبيقات بديلة في الذاكرة أو بمحاكيات Mockito في اختبارات الوحدة.
- استخدم
@Tag("integration")لفصل اختبارات التكامل كي تعمل في CI دون أن تُبطّئ كل بناء محلّي. - لا تستخدم
Thread.sleep()في اختبار لانتظار نتيجة غير متزامنة — استخدمAwaitilityأوCompletableFuture.get(timeout). - تجنّب
new Date()أوInstant.now()مباشرةً؛ أدخِلClockحقنًا كي تتحكّم الاختبارات في الوقت.
الخلاصة
سمات مجموعة الاختبارات عالية الجودة: أسماء تُقرأ كمواصفات، تحقّق منطقي واحد لكل اختبار، عزل كامل لكل حالة، هيكل AAA، اختبارات تستهدف السلوك العام القابل للملاحظة لا تفاصيل التنفيذ الخاصة، وزمن تشغيل سريع وحتمي. استوعب هذه الممارسات وستصبح مجموعة اختباراتك شبكة أمان تثق بها — شبكة تُسرّع التطوير بدلًا من أن تُعيقه.