الاختبار باستخدام JUnit 5 وMockito

أساسيات JUnit 5

15 دقيقة الدرس 2 من 13

أساسيات JUnit 5

JUnit 5 هو إطار الاختبار المعياري الفعلي على JVM. إنه ليس مكتبة واحدة بل بنية تتكون من ثلاثة وحدات: JUnit Platform (منصة الإطلاق وواجهة برمجة المحركات)، وJUnit Jupiter (نموذج البرمجة الجديد والتعليقات التوضيحية التي تكتبها يوميًا)، وJUnit Vintage (المشغّل المتوافق مع الإصدار السابق لاختبارات JUnit 3/4). في العمل اليومي تتعامل بشكل شبه حصري مع Jupiter.

إضافة JUnit 5 إلى مشروعك

تتضمن أدوات البناء الحديثة JUnit 5 تلقائيًا عند إنشاء مشروع، لكن من المفيد معرفة الاعتماديات الصريحة. في Maven أضف BOM واعتمادية واحدة بنطاق test:

<!-- pom.xml --> <dependencyManagement> <dependencies> <dependency> <groupId>org.junit</groupId> <artifactId>junit-bom</artifactId> <version>5.10.2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> </dependencies>

مع Gradle (Kotlin DSL):

// build.gradle.kts dependencies { testImplementation(platform("org.junit:junit-bom:5.10.2")) testImplementation("org.junit.jupiter:junit-jupiter") } tasks.test { useJUnitPlatform() // يخبر Gradle باكتشاف اختبارات Jupiter }
useJUnitPlatform() إلزامي في Gradle. بدونه يستخدم Gradle مشغّل JUnit 4 المدمج ويتخطى جميع اختبارات Jupiter بصمت — وهو إخفاق مربك بشكل مشهور حيث تبدو نتيجة البناء ناجحة لكن لم يُنفَّذ أي اختبار فعليًا.

التعليق التوضيحي @Test

يُعدّ @Test العلامة الأساسية التي تحوّل دالة عادية إلى حالة اختبار. في Jupiter يقع في حزمة org.junit.jupiter.api — وهي حزمة مختلفة عن org.junit.Test القديمة في JUnit 4، لذلك إذا استوردت بيئة التطوير الحزمة الخاطئة سيتجاهل مشغّل الاختبارات الدالة بصمت.

قواعد دالة @Test الصالحة:

  • يجب أن تكون الكلاس غير مجردة (non-abstract) وأن يكون لها مُنشئ واحد يمكن الوصول إليه (أو لا شيء مما يُشغّل المُنشئ الافتراضي).
  • يجب أن تكون الدالة غير خاصة (non-private) وأن تُعيد void. لا يشترط JUnit 5 public — والمستوى الافتراضي (package-private) هو الأسلوب المفضّل.
  • يجب ألا تأخذ الدالة أي معاملات إلا إذا كنت تستخدم محلّلات المعاملات (مُغطّاة في دروس لاحقة).

أول اختبار

فيما يلي كلاس مكتفية بذاتها تختبر دالة مساعدة صغيرة:

package com.example.util; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class StringUtilsTest { @Test void reverseOfEmptyStringIsEmpty() { String result = StringUtils.reverse(""); assertEquals("", result); } @Test void reverseOfSingleCharIsItself() { assertEquals("a", StringUtils.reverse("a")); } @Test void reverseOfPalindromeIsPalindrome() { String palindrome = "racecar"; assertEquals(palindrome, StringUtils.reverse(palindrome)); } @Test void reverseOfRegularWordIsCorrect() { assertEquals("olleh", StringUtils.reverse("hello")); } }
سمِّ الاختبارات كجمل تصف السلوك لا التنفيذ. reverseOfEmptyStringIsEmpty() يخبرك فورًا بما سيفشل في حال إخفاق الاختبار. أسماء مثل test1() أو testReverse() لا تفيد بشيء. تستخدم منصة JUnit اسم الدالة كاسم للعرض في التقارير، لذا تهمّ القراءة في المصدر وفي مخرجات CI.

كيف تُنفَّذ الاختبارات

فهم نموذج التنفيذ يمنع فئة كاملة من أخطاء الترتيب الخفية.

  1. الاكتشاف. تمسح منصة JUnit مسار الكلاسات بحثًا عن كلاسات تحتوي دوالًا موسومة بـ @Test. بالاتفاق تقع هذه الكلاسات تحت src/test/java وتعكس هيكل الحزمة للكود الإنتاجي الذي تختبره.
  2. الإنشاء. يُنشئ JUnit 5 نسخة جديدة من كلاس الاختبار لكل دالة اختبار بشكل افتراضي. هذا القرار المتعمد يمنع تسرّب الحالة المتغيّرة المشتركة بين الاختبارات — وهو مصدر رئيسي للاختبارات المتقلبة في JUnit 4 التي كانت تُعيد استخدام نسخة واحدة لكل الكلاس.
  3. تنفيذ الدالة. تستدعي المنصة الدالة. إذا اكتملت دون رمي استثناء ينجح الاختبار. إذا هرب أي استثناء — بما فيه AssertionError من تأكيد فاشل — يُعلَّم الاختبار بالفشل.
  4. إعداد التقرير. تُجمع النتائج وتُعرض بواسطة مشغّل الاختبارات في بيئة التطوير، أو Maven Surefire، أو تقرير HTML لـ Gradle.
// توضيح الإنشاء لكل دالة class InstantiationDemoTest { // تُنشأ ArrayList جديدة لكل دالة اختبار private final List<String> items = new ArrayList<>(); @Test void firstTest() { items.add("one"); assertEquals(1, items.size()); // ينجح دائمًا } @Test void secondTest() { // items قائمة فارغة جديدة؛ إضافة firstTest لم تحدث هنا أبدًا assertEquals(0, items.size()); // ينجح أيضًا } }

ترتيب تنفيذ الاختبارات

لا يضمن JUnit 5 افتراضيًا الترتيب الذي تُنفَّذ به الدوال داخل كلاس. الترتيب حتمي عبر التشغيلات (استنادًا إلى تجزئة اسم الدالة)، لكنه ليس أبجديًا ولا بترتيب التصريح. هذا مقصود: الاختبارات التي تنجح فقط بترتيب معين تُخفي اعتمادية خفية وليست اختبارات وحدة حقيقية.

إذا احتجت ترتيبًا محددًا — لاختبارات التكامل أو السيناريوهات — ضع تعليقًا توضيحيًا على الكلاس بـ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) ثم استخدم @Order(1)، @Order(2)، إلخ على كل دالة. احتفظ بهذا للسيناريوهات الحقيقية، لا كعكاز لاختبارات وحدة سيئة العزل.

أسماء العرض

يتيح لك التعليق التوضيحي @DisplayName استبدال اسم الدالة بسلسلة نصية مقروءة تتضمن مسافات أو أحرفًا خاصة — مفيد عندما لا يستطيع اسم الدالة التعبير الكامل عن النية:

@Test @DisplayName("reverse(null) should throw NullPointerException") void reverseNullThrows() { assertThrows(NullPointerException.class, () -> StringUtils.reverse(null)); }
لا تستخدم @DisplayName كذريعة لاسم دالة سيء. تبحث بيئات التطوير في مخرجات الاختبار باسم الدالة؛ إذا كانت تُسمى test42() تفقد هذا التنقل. اسم دالة جيد مع @DisplayName اختياري للتقرير هو المزيج الصحيح.

تعطيل اختبار

استخدم @Disabled (مع سلسلة سبب إلزامية) عندما يجب تخطّي اختبار مؤقتًا — لا تحذفه أبدًا ولا تعلّق عليه بتعليق كود:

@Test @Disabled("Temporarily disabled: upstream CSV parser bug PROJ-1234, fix expected in v3.2") void parsesCsvWithQuotedCommas() { // ... }

الاختبار المعطَّل لا يزال يظهر في التقرير كـ skipped، مما يحافظ على الوضوح بأن شيئًا ما لا يعمل عن قصد. حذف الاختبار يُزيل هذا الوضوح بصمت.

الخلاصة

Jupiter في JUnit 5 هو طبقة التعليقات التوضيحية التي تتفاعل معها يوميًا. يُعلِّم @Test دالة غير خاصة وتُعيد void وليس لها معاملات كحالة اختبار. تُنشأ نسخة جديدة من الكلاس لكل دالة مما يُزيل التلوث بين الاختبارات. تنجح الاختبارات إذا لم يهرب أي استثناء وتفشل عند أي استثناء مرمي بما فيه إخفاقات التأكيد. الأسماء الجيدة للدوال والاختياري @DisplayName تُبقي تقارير الاختبارات مقروءة. في الدرس التالي سنتعمق في واجهة برمجة التأكيدات في JUnit 5، بما فيها التأكيدات المجمّعة وتأكيدات الاستثناءات وقيود المهلة الزمنية.