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

@SpringBootTest وسياق الاختبار

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

@SpringBootTest وسياق الاختبار

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

ما الذي يفعله @SpringBootTest فعلًا

عند تزيين فئة اختبار بـ@SpringBootTest، تقوم البنية التحتية لاختبارات Spring Boot بما يلي:

  1. تحديد موقع فئة @SpringBootApplication الرئيسية بالصعود في شجرة الحزم انطلاقًا من فئة الاختبار.
  2. بناء ApplicationContext الكامل — بمسح المكونات، ومعالجة فئات @Configuration، وتشغيل التهيئة التلقائية، وربط application.properties / application.yml.
  3. حقن الحبوب في فئة الاختبار عبر @Autowired، تمامًا كأي حبة يديرها Spring.
لا يبدأ أي خادم افتراضيًا. ينشئ @SpringBootTest السياق الكامل لكنه لا يستمع على أي منفذ ما لم تحدد webEnvironment. بالنسبة للاختبارات التي تحتاج فقط إلى استدعاء حبوب الخدمة أو المستودع مباشرةً، فإن الوضع الافتراضي (بلا خادم) أسرع وكافٍ تمامًا.

أبسط اختبار تكامل ممكن يبدو هكذا:

import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest class OrderServiceIntegrationTest { @Autowired private OrderService orderService; // حبة حقيقية من الحاوية @Test void shouldCalculateTotalCorrectly() { Order order = orderService.findById(1L); assertThat(order.getTotal()).isPositive(); } }

السمة webEnvironment

يقبل @SpringBootTest معامل webEnvironment الذي يتحكم في ما إذا كان خادم مُضمَّن سيُشغَّل وكيف:

  • MOCK (الافتراضي): يُحمِّل WebApplicationContext ببيئة Servlet وهمية. لا منفذ حقيقي. يُستخدم مع MockMvc.
  • RANDOM_PORT: يُشغِّل الخادم المُضمَّن على منفذ عشوائي مجاني. مثالي لاختبارات HTTP الكاملة من البداية إلى النهاية باستخدام TestRestTemplate أو WebTestClient. يُحقَن المنفذ عبر @LocalServerPort.
  • DEFINED_PORT: يبدأ على المنفذ المحدد في application.properties (الافتراضي 8080). محفوف بالمخاطر في بيئة CI — فلو عمل اختباران متوازيان سيتعارضان.
  • NONE: يُحمِّل ApplicationContext بلا بيئة ويب على الإطلاق. مفيد لاختبار حبوب طبقة الخدمة في تطبيق غير ويب.
// اختبار دورة HTTP كاملة على منفذ عشوائي @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class OrderApiIntegrationTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Test void getOrderReturns200() { ResponseEntity<OrderDto> response = restTemplate.getForEntity("http://localhost:" + port + "/api/orders/1", OrderDto.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } }

التخزين المؤقت للسياق — أهم ميزة لتحسين الأداء

يستغرق تهيئة سياق Spring وقتًا: مسح المكونات، والتهيئة التلقائية، وإنشاء تجمع اتصالات HikariCP، والتحقق من مخطط Hibernate — قد يضيف كل هذا من 2 إلى 5 ثوانٍ لكل سياق. لو أنشأت كل فئة اختبار سياقها الخاص، قد تنفق مجموعة مكونة من 200 اختبار وقتًا أطول في الإعداد من الأوقات التي تقضيها في التحقق الفعلي.

تحل بنية اختبارات Spring هذه المشكلة عبر التخزين المؤقت للسياق: يُشغَّل السياق مرة واحدة لكل مفتاح تهيئة فريد ويُعاد استخدامه من قِبل كل فئة اختبار تشترك في ذلك المفتاح. يُشتق مفتاح الذاكرة المؤقتة من:

  • مجموعة تعليقات تهيئة السياق (@SpringBootTest، @ContextConfiguration، إلخ).
  • قيمة webEnvironment.
  • أي ملفات تعريف نشطة (@ActiveProfiles).
  • أي تجاوزات للخصائص (سمة properties أو @TestPropertySource).
  • أي تصريحات @MockBean أو @SpyBean (كل مجموعة فريدة تنشئ سياقًا منفصلًا).
صمِّم مجموعة اختباراتك لإعادة استخدام السياق. اجمع الاختبارات التي تشترك في نفس التهيئة في نفس فئة الاختبار أو في فئات ترث من فئة أساسية مشتركة. أبقِ تصريحات @MockBean في الحد الأدنى اللازم — فكل مجموعة جديدة من @MockBean تفرض إنشاء سياق جديد.
// فئة أساسية — يُحمَّل السياق مرة واحدة لجميع الفئات الفرعية التي تشترك في هذا الإعداد @SpringBootTest @ActiveProfiles("test") abstract class AbstractIntegrationTest { // مساعدات مشتركة، حبوب @Autowired مشتركة، تنظيف @BeforeEach } // تُعيد استخدام السياق المخزَّن مؤقتًا لـ @SpringBootTest + الملف التعريفي "test" class OrderServiceTest extends AbstractIntegrationTest { ... } // تُعيد استخدام نفس السياق المخزَّن مؤقتًا — لا تكلفة تشغيل إضافية class ProductServiceTest extends AbstractIntegrationTest { ... }

استخدام ملف خصائص تطبيق اختبار مخصص

يجب ألا تُستخدم قواعد بيانات الإنتاج ومفاتيح API في الاختبارات. ضع ملف src/test/resources/application.properties (أو application-test.yml) بجوار مصادر الاختبار. يلتقطه Spring Boot تلقائيًا ويمنحه الأولوية على الملف الرئيسي عند تشغيل الاختبارات.

# src/test/resources/application.properties # استخدام قاعدة بيانات H2 في الذاكرة — لا Docker، لا تنظيف مطلوب spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 spring.datasource.driver-class-name=org.h2.Driver spring.jpa.hibernate.ddl-auto=create-drop # إخفاء سجلات SQL الصاخبة أثناء الاختبارات spring.jpa.show-sql=false logging.level.org.hibernate=WARN

إن احتاجت مجرد حفنة من الاختبارات خصائص خاصة، فاستخدم سمة properties المضمَّنة بدلًا من ملف — فهي تُبقي التجاوز مرئيًا بجوار التعليق مباشرةً:

@SpringBootTest(properties = { "app.feature.payment-gateway=stub", "spring.jpa.hibernate.ddl-auto=create-drop" }) class PaymentIntegrationTest { ... }
تجنّب @DirtiesContext ما لم تحتجه فعلًا. يُخبر هذا التعليق Spring بتدمير السياق وإعادة بنائه بعد الاختبار الموسوم (فئة أو طريقة). يضمن العزل لكنه يُلغي التخزين المؤقت للسياق كليًا — وهو أحد أكثر أسباب بطء مجموعات الاختبار شيوعًا. افضّل التراجع عن قاعدة البيانات (عبر @Transactional على طريقة الاختبار) أو إدارة دورة حياة Testcontainers لتحقيق العزل.

متى تستخدم @SpringBootTest مقابل اختبارات الشرائح

يوفّر Spring Boot عدة شرائح اختبار مركّزة — @WebMvcTest، و@DataJpaTest، و@JsonTest، وغيرها — تُحمِّل فقط الجزء من السياق المناسب لطبقة واحدة. يكون @SpringBootTest هو الخيار الصحيح حين:

  • تحتاج إلى اختبار تكامل طبقات متعددة (مثلًا: المتحكم → الخدمة → المستودع).
  • تختبر سلوك التهيئة التلقائية — للتحقق من أن application.properties يُشغِّل حبة معقدة بشكل صحيح.
  • تكتب اختبارات دخان تؤكد تحميل سياق التطبيق بلا أخطاء (contextLoads()).

بالنسبة للاختبارات التي تستهدف طبقة واحدة بمعزل عن غيرها، افضّل تعليق الشريحة المناسب — فهو يبدأ بشكل أسرع ويُحدد الأعطال في الطبقة الصحيحة. تستخدم مجموعة الاختبارات المُنظَّمة جيدًا كليهما: اختبارات شرائح لغالبية التغطية وعددًا صغيرًا من اختبارات التكامل بـ@SpringBootTest للتحقق من التوصيل الشامل من البداية إلى النهاية.

الخلاصة

يُشغِّل @SpringBootTest ApplicationContext الكامل لاختباراتك مع تحكم دقيق في بيئة الويب عبر سمة webEnvironment. يعني التخزين المؤقت للسياق أن تكلفة التشغيل المرتفعة تُدفع مرة واحدة لكل تهيئة فريدة — لذا فإن هيكلة اختباراتك لمشاركة التهيئة هي أقوى رافعة للحفاظ على سرعة مجموعة اختبارات التكامل. استخدم ملف application.properties مخصصًا في src/test/resources لاستبدال بنية الإنتاج الأساسية (قواعد البيانات، والطوابير، وواجهات API الخارجية) ببدائل خفيفة للاختبار، وارجع إلى @SpringBootTest عندما تحتاج إلى التحقق من التوصيل عبر الطبقات لا من سلوك مكونات معزولة.