اختبار تطبيقات Spring Boot

الاختبار في Spring Boot

18 دقيقة الدرس 1 من 13

الاختبار في Spring Boot

كتابة كود يعمل مرة واحدة أمر سهل. أما كتابة كود يظل يعمل عبر عمليات إعادة الهيكلة وترقيات التبعيات وتغيرات الفريق فهو التحدي الهندسي الحقيقي. الاختبارات الآلية هي الممارسة التي تجعل ذلك ممكنًا، وتشحن Spring Boot بمنظومة اختبار متكاملة من الصندوق مباشرةً. يُؤطّر هذا الدرس المشكلة ويُقدّم المفاهيم الأساسية ويستعرض كل مكوّن تجلبه تبعية spring-boot-starter-test إلى مسار الفئات، حتى تعرف ما تعمل معه قبل أن تكتب أول @Test.

لماذا يهم الاختبار أكثر في تطبيق Spring

خدمة Spring Boot هي تأليف من حبوب (beans) متعاونة كثيرة — متحكمات وخدمات ومستودعات ومستمعات أحداث ومهام مجدولة — مترابطة بحاوية IoC. يمكن لأي حبّة منها أن تنكسر بصمت إذا تغيّر عقد التبعية. تكشف الاختبارات الآلية تلك الكسور في ثوانٍ لا خلال حادثة إنتاجية في الثانية صباحًا.

ثمة فائدة أخرى أدق: الكود القابل للاختبار هو دائمًا تقريبًا كودٌ أفضل تصميمًا. الخدمة التي يصعب اختبارها بمعزل عن غيرها إشارةٌ إلى أنها تحمل مسؤوليات كثيرة أو مقترنة اقترانًا شديدًا. يدفع فعل كتابة الاختبارات تصميمك نحو مكوّنات أصغر وأنظف وأكثر قابلية للتأليف.

هرم الاختبار

يُعدّ هرم الاختبار نموذجًا ذهنيًا قدّمه مايك كوهن يصف التوزيع المثالي للاختبارات عبر ثلاث طبقات:

  • اختبارات الوحدة (القاعدة — العريضة): تختبر فئة أو دالة واحدة في عزلة تامة. تُستبدَل جميع المتعاونون بأضعاف اختبارية (mocks أو stubs). هذه الاختبارات سريعة (أقل من ميلي ثانية كل منها) وكثيرة وتشكّل العمود الفقري لمجموعتك.
  • اختبارات التكامل (الوسط — أضيق): تختبر مكوّنَين حقيقيَّين أو أكثر يعملان معًا — مثلًا خدمة تتفاعل مع قاعدة بيانات حقيقية أو في الذاكرة، أو متحكم مرتبط بطبقة خدمة حقيقية. هي أبطأ وأقل عددًا.
  • اختبارات من طرف إلى طرف (القمة — الضيقة): تختبر التطبيق بأكمله من الخارج — طلب HTTP يضرب الخادم ويمر بكل طبقة وصولًا إلى قاعدة البيانات. هي الأبطأ والأكثر هشاشة ويجب أن تكون الأقل.
الهرم دليل لا قانون. قد يحتوي مشروع Spring Boot نموذجي على 200 اختبار وحدة و40 اختبار شريحة/تكامل و10 اختبارات دخان شاملة. إذا عكست هذه النسبة — اختبارات كثيرة بطيئة من طرف إلى طرف واختبارات وحدة قليلة — ينهار حلقة التغذية الراجعة ويتوقف المطوّرون عن تشغيل المجموعة محليًا.

صُمّمت أدوات اختبار Spring Boot بحيث يمكنك كتابة اختبارات على كل مستوى من مستويات الهرم دون كود تجهيزي. يتولى الإطار تشغيل السياق وربطه نيابةً عنك.

ما توفره spring-boot-starter-test

أضف مُشغِّل الاختبار إلى ملف pom.xml بنطاق test:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>

هذه التبعية الوحيدة تسحب بشكل انتقالي مجموعة منتقاة بعناية من المكتبات. فهم كل منها يخبرك بأي أداة تمد يدك في سيناريو اختبار معيّن.

JUnit 5 (Jupiter)

JUnit 5 هو مشغّل الاختبار — المحرك الذي يكتشف ويُنفّذ دوال الاختبار. حلّ محل JUnit 4 باعتباره الإعداد الافتراضي لـ Spring Boot منذ الإصدار 2.2. التعليقات التوضيحية الرئيسية التي ستستخدمها باستمرار:

  • @Test — تُعلّم الدالة باعتبارها حالة اختبار.
  • @BeforeEach / @AfterEach — إعداد وتنظيف حول كل دالة اختبار.
  • @BeforeAll / @AfterAll — دوال ثابتة تعمل مرة واحدة لكل فئة اختبار (للإعداد المكلف كتشغيل حاوية).
  • @DisplayName — اسم مقروء يظهر في IDE وتقارير CI.
  • @Nested — يُجمّع الاختبارات ذات الصلة داخل فئة داخلية لتحسين القراءة.
  • @ParameterizedTest مع @ValueSource / @CsvSource / @MethodSource — تشغيل نفس الاختبار بمدخلات متعددة دون نسخ ولصق.
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @DisplayName("اختبارات وحدة PriceCalculator") class PriceCalculatorTest { private final PriceCalculator calc = new PriceCalculator(); @Test @DisplayName("يطبق خصم 10% عندما يتجاوز الطلب الحد") void appliesDiscount() { double result = calc.calculate(200.0, 0.10); assertEquals(180.0, result, 0.001, "خصم 10% من 200 يجب أن يساوي 180"); } @Test @DisplayName("يرفض نسبة الخصم السالبة") void rejectsNegativeDiscount() { assertThrows(IllegalArgumentException.class, () -> calc.calculate(100.0, -0.05)); } }

AssertJ — التأكيدات السلسلة

يشحن JUnit 5 بدوال Assertions أساسية، لكن AssertJ مضمّن في المُشغِّل ومُفضَّل بشكل شبه عالمي لأن واجهته البرمجية السلسلة تُنتج رسائل فشل أوضح بكثير.

import static org.assertj.core.api.Assertions.*; // بدلًا من: assertEquals(3, list.size()); assertTrue(list.contains("Alice")); // مع AssertJ: assertThat(list) .hasSize(3) .contains("Alice") .doesNotContain("Bob");

عند فشل اختبار، تطبع AssertJ بالضبط ما وجدته مقابل ما توقّعته، بما في ذلك محتويات القائمة الكاملة — لا مجرد "توقّعت true لكن كان false".

Mockito — محاكاة المتعاونين

تتيح لك Mockito استبدال التبعيات الحقيقية بأضعاف خاضعة للتحكم على مستوى اختبار الوحدة. ستستخدم ثلاثة أنماط باستمرار:

  • mock(SomeClass.class) — ينشئ mock يعيد القيم الافتراضية (null، 0، مجموعات فارغة) ما لم تُعدّه.
  • when(...).thenReturn(...) — يُعدّ دالة لإعادة قيمة محددة.
  • verify(...) — يؤكد أن دالة استُدعيَت بالوسيطات المتوقعة.
import org.junit.jupiter.api.Test; import static org.mockito.Mockito.*; import static org.assertj.core.api.Assertions.*; class OrderServiceTest { private final OrderRepository repo = mock(OrderRepository.class); private final OrderService service = new OrderService(repo); @Test void findsOrderById() { Order expected = new Order(42L, "PENDING"); when(repo.findById(42L)).thenReturn(Optional.of(expected)); Order actual = service.getOrder(42L); assertThat(actual.getStatus()).isEqualTo("PENDING"); verify(repo).findById(42L); // تأكد أن المستودع استُدعي } }
فضّل حقن المنشئ في حبوبك. يمكن إنشاء خدمة تستقبل تبعياتها عبر المنشئ في اختبار JUnit عادي دون أي سياق Spring — فقط new OrderService(mockRepo). هذا أسرع اختبار ممكن وهو الذي يجب أن تلجأ إليه أولًا.

Hamcrest — مكتبة المطابقات

مطابقات Hamcrest موجودة أيضًا في مسار الفئات (سابقة لـ AssertJ). قد تصادفها في الكود القديم أو في تأكيدات نتائج اختبار Spring MVC (MockMvcResultMatchers يستخدم Hamcrest افتراضيًا). كلاهما يتعايشان دون تعارض.

JSONassert وJsonPath

عندما تحتاج اختباراتك إلى التحقق من بنية استجابات JSON من نقاط نهاية REST، يتضمّن المُشغِّل مساعدَين:

  • JSONassert — يقارن سلاسل JSON هيكليًا متجاهلًا التنسيق وترتيب الحقول.
  • JsonPath — يتيح استخراج قيم من مستند JSON باستخدام تعبيرات المسار (مثلًا $.orders[0].id). يتكامل MockMvc في Spring مباشرةً مع JsonPath.

spring-test — طبقة تكامل Spring

توفر وحدة spring-test (جزء من Spring Framework الأساسي) البنية التحتية التي تدمج JUnit 5 مع حاوية IoC:

  • @ExtendWith(SpringExtension.class) — امتداد JUnit 5 يُشغّل سياق Spring لفئات الاختبار المُعلَّمة (مُضمَّن تلقائيًا في @SpringBootTest وتعليقات الشريحة).
  • @ContextConfiguration — تعليق منخفض المستوى لتحديد فئات التهيئة التي يجب تحميلها.
  • MockMvc — طبقة HTTP مخصصة للاختبار ترسل الطلبات إلى متحكماتك دون تشغيل حاوية servlet فعلية.
  • TestRestTemplate — مغلّف حول RestTemplate لاختبارات HTTP كاملة الرصة التي تشغّل خادمًا فعلًا.
  • @TestPropertySource / @DynamicPropertySource — تجاوز خصائص التهيئة لفئة اختبار محددة.

تعليقات الشريحة — ميزة Spring Boot الاختبارية

تضيف Spring Boot طبقة فوق spring-test: تعليقات الشريحة. تُشغّل كل شريحة فقط المجموعة الفرعية من سياق التطبيق ذات الصلة بطبقة واحدة مما يجعل الاختبارات أسرع وأكثر تركيزًا:

  • @WebMvcTest — يحمّل طبقة الويب فقط (متحكمات وفلاتر ومحوّلات). لا حبوب خدمة أو مستودع.
  • @DataJpaTest — يحمّل مستودعات JPA فقط وقاعدة بيانات مدمجة. لا متحكمات.
  • @JsonTest — يحمّل مكوّنات التسلسل/إلغاء التسلسل JSON فقط.
  • @SpringBootTest — يحمّل سياق التطبيق الكامل. استخدمه لاختبارات التكامل الحقيقية.
تجنّب اللجوء إلى @SpringBootTest بشكل منعكس. يستغرق تشغيل السياق الكامل عدة ثوانٍ ويحمّل كل حبّة ومصدر بيانات وتكامل خارجي. لفئة بها 30 دالة اختبار تحتاج فقط إلى متحكم، يتضاعف هذا العبء بشكل مؤلم. استخدم أضيق شريحة تغطي ما تحتاج اختباره.

الخلاصة

يمنحك هرم الاختبار استراتيجية: اختبارات وحدة كثيرة وسريعة، واختبارات تكامل أقل، وعدد صغير من الاختبارات الشاملة. تجلب تبعية spring-boot-starter-test JUnit 5 كمشغّل، وAssertJ للتأكيدات السلسلة، وMockito للأضعاف، وتعليقات شريحة Spring الخاصة حتى تحمّل القدر المناسب من السياق لكل اختبار. في الدرس القادم ستضع JUnit 5 وMockito في الاستخدام لكتابة اختبارات وحدة لفئة خدمة — دون أي سياق Spring مطلوب.