Testcontainers والاعتماديات الحقيقية
Testcontainers والاعتماديات الحقيقية
كل طبقة اختبار استخدمتها حتى الآن تتنازل عن الدقة مقابل السرعة. يعمل @DataJpaTest على H2، ويستبدل @MockBean الخدمات بأكملها، ولا يلمس @WebMvcTest قاعدة البيانات قط. كثيرًا ما تكون هذه التنازلات صحيحة — لكنها أحيانًا تُخفي أخطاءً حقيقية. استعلام يعمل على لهجة H2 من SQL قد يفشل بصمت على PostgreSQL 16. وترحيل Flyway غير متوافق مع مخطط الإنتاج الخاص بك لن يُختبر أبدًا. وفهرس مهم للأداء يكون غائبًا لأن المحرك في الذاكرة يتجاهله.
تسدّ Testcontainers هذه الفجوة بتشغيل حاويات Docker حقيقية وقابلة للتخلص — PostgreSQL وMySQL وRedis وKafka وأي شيء آخر — مباشرةً داخل تشغيل اختبار JUnit 5 الخاص بك. يتصل كودك بنفس المحرك الذي سيواجهه في الإنتاج، مع إدارة دورة حياة الحاوية تلقائيًا: تبدأ قبل أول اختبار وتُدمَّر بعد آخره.
لماذا لا تستخدم H2 فقط؟
H2 سريع ولا يحتاج أي بنية تحتية، مما يجعله مثاليًا لاختبارات الثبات على مستوى الوحدة. لكن وضع التوافق فيه غير مكتمل. من أبرز حالات الفشل عند التبديل من H2 إلى محرك حقيقي:
- دوال خاصة بـ PostgreSQL (
gen_random_uuid()، معاملات JSON، دوال النوافذ) التي لا تُنفّذها H2. - المعرّفات الحساسة لحالة الأحرف — PostgreSQL يطوي المعرّفات غير المقتبسة إلى حروف صغيرة؛ H2 يطويها إلى حروف كبيرة.
- اختلافات توقيت إنفاذ المفاتيح الأجنبية الصارمة.
- ترحيلات Flyway أو Liquibase التي تحتوي على DDL خاص بقاعدة البيانات يرفضها H2.
- اختلافات لهجة Hibernate 6 — نفس JPQL يُترجَم إلى SQL مختلف على لهجات مختلفة، وقد تظهر مشكلات في مخطط الاستعلام على اللهجة الحقيقية لا تكون مرئية في H2.
إضافة الاعتمادية
تنشر Testcontainers وحدة تكامل مع Spring Boot تربط كل شيء تلقائيًا. أضفها إلى pom.xml (نطاق الاختبار فقط):
تُدير Spring Boot إصدار BOM الخاص بـ Testcontainers من خلال إدارة الاعتماديات الخاصة بها، لذا لا تحتاج إلى تحديد الإصدارات صراحةً عند استخدام POM الأصلي لـ Spring Boot.
نهج ServiceConnection (Spring Boot 3.1 وما بعده)
أنظف طريقة لدمج Testcontainers مع Spring Boot 3.1 أو أحدث هي تعليق @ServiceConnection. تُعلن عن حاوية bean في فئة تهيئة الاختبار، وتُعلّقها بـ @ServiceConnection، فيتجاوز Spring Boot تلقائيًا spring.datasource.* (أو مجموعة الخصائص ذات الصلة للخدمات الأخرى) للإشارة إلى الحاوية العاملة. لا حاجة لتجاوزات الخصائص اليدوية ولا لأسلوب DynamicPropertySource المتكرر.
الإعلان عن الحاوية بوصفها static مقصود. تبدأ الحاوية الساكنة مرة واحدة لجميع الاختبارات في الفئة وتُعاد استخدامها عبر أساليب الاختبار، وهو أرخص بكثير من إنشاء حاوية جديدة لكل اختبار.
اختبار تكامل كامل
إليك مثالًا واقعيًا. يمتلك التطبيق كيان Order، وOrderRepository يمتد JpaRepository، وترحيل Flyway ينشئ المخطط. نريد التحقق من أن دورة الحفظ-وإعادة التحميل عبر محرك PostgreSQL الحقيقي تعمل بشكل صحيح، وأن ترحيل Flyway متوافق.
(1) يُخبر @AutoConfigureTestDatabase(replace = NONE) الـ @DataJpaTest بعدم استبدال DataSource المُهيَّأ بمخطط مضمَّن خاص به. بدون هذا، سيستبدل Spring Boot ما زال H2 حتى لو أعلنت حاوية.
replace = NONE. إنه الخطأ الأكثر شيوعًا عند الجمع بين @DataJpaTest وTestcontainers. إذا حذفته، تنجح اختباراتك ضد H2 مع تجاهل حاوية PostgreSQL التي أبدأتها للتو بصمت.
إعادة استخدام الحاوية عبر فئات الاختبار
يستغرق بدء حاوية PostgreSQL نحو 2–4 ثوانٍ — مقبول لفئة واحدة لكنه مؤلم إذا كان لديك 20 فئة اختبار تعمل كل منها حاويتها الخاصة. النمط المعياري هو فئة أساسية مشتركة تحمل الحاوية كحقل static، وهو ما تبقيه JUnit 5 + Testcontainers حيًّا طوال مدة عملية JVM:
بما أن POSTGRES حقل static final على الفئة الأصلية، تشترك جميع الفئات الفرعية في نفس نسخة الحاوية. تُدير امتداد @Testcontainers على الفئة الأصلية دورة حياتها.
postgres:16-alpine بدلًا من postgres:latest. استخدام latest يعني أن تحديث Docker Hub يمكنه تغيير المحرك الذي تعمل عليه اختباراتك بصمت بين عمليات CI، مما يتسبب في فشل متقطع يصعب جدًا تشخيصه.
مقايضات الأداء
اختبارات Testcontainers أبطأ من اختبارات الوحدة الخالصة أو اختبارات @DataJpaTest المدعومة بـ H2. إليك مقارنة واقعية لوحدة متوسطة الحجم:
- اختبار وحدة (Mockito، بدون سياق Spring): ~5–50 مللي ثانية لكل فئة.
- @DataJpaTest مع H2: ~1–3 ثوانٍ لتحميل سياق الشريحة.
- @DataJpaTest مع Testcontainers (حاوية مشتركة): ~3–6 ثوانٍ للفئة الأولى، ثم تكاد لا توجد تكلفة إضافية للفئات اللاحقة التي تشترك في نفس الحاوية.
- @SpringBootTest مع Testcontainers (سياق كامل): ~8–15 ثانية للفئة الأولى.
توضّح هذه الأرقام أن إعادة استخدام الحاوية (نمط الفئة الأساسية المشتركة أعلاه) وتخزين سياق Spring مؤقتًا كلاهما ضروريان. يخزّن Spring سياق التطبيق مؤقتًا عبر الاختبارات التي تستخدم تهيئة متطابقة، لذا يُحمَّل السياق مرة واحدة ويُعاد استخدامه، لا يُعاد إنشاؤه لكل فئة اختبار.
ماذا يمكن لـ Testcontainers تشغيله؟
Testcontainers ليست مقتصرة على قواعد البيانات العلائقية. النمط نفسه يعمل مع أي خدمة لها صورة Docker:
KafkaContainer— اختبر الكود المبني على الأحداث ضد وسيط حقيقي.GenericContainer("redis:7-alpine")— اختبر تكامل طبقة التخزين المؤقت.LocalStackContainer— خدمات AWS (S3، SQS، SNS) محليًا.MongoDBContainer— اختبر مستودعات MongoDB بدون Atlas.
يدعم تعليق @ServiceConnection كثيرًا من هذه الخدمات مباشرةً؛ وللبقية تعود إلى آلية @DynamicPropertySource الأقدم.
الخلاصة
تسدّ Testcontainers الفجوة بين اختبارات الذاكرة السريعة لكن غير الدقيقة والاختبارات البطيئة لكن الدقيقة ضد بنية تحتية خارجية مشتركة. الخطوات الأساسية هي: إضافة اعتماديات Maven الثلاث، والإعلان عن حقل static @Container @ServiceConnection، وإضافة @AutoConfigureTestDatabase(replace = NONE) عند استخدام @DataJpaTest، واستخراج فئة أساسية مشتركة لتجنب إعادة تشغيل الحاوية لكل فئة اختبار. النتيجة هي مجموعة اختبارات تختبر SQL الحقيقي والترحيلات الحقيقية ولهجة Hibernate الحقيقية — مما يكتشف صنفًا كاملًا من الأخطاء يُخفيها H2 بصمت.