TestContainers دليل مرجعي سريع
التثبيت
المتطلبات الأساسية
| المتطلب | الإصدار | أمر التحقق |
|---|
| Docker | أحدث إصدار مستقر | docker --version |
| Java JDK | ٨ أو أعلى | java -version |
| Maven/Gradle | أحدث | mvn -v or gradle -v |
اعتمادات Maven
<!-- Core TestContainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 Integration -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<!-- Database Modules -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
اعتمادات Gradle
// Kotlin DSL
testImplementation("org.testcontainers:testcontainers:1.19.3")
testImplementation("org.testcontainers:junit-jupiter:1.19.3")
testImplementation("org.testcontainers:postgresql:1.19.3")
testImplementation("org.testcontainers:mysql:1.19.3")
testImplementation("org.testcontainers:kafka:1.19.3")
// Groovy DSL
testImplementation 'org.testcontainers:testcontainers:1.19.3'
testImplementation 'org.testcontainers:junit-jupiter:1.19.3'
testImplementation 'org.testcontainers:postgresql:1.19.3'
إعداد Docker حسب المنصة
| منصة | أمر التثبيت |
|---|
| Ubuntu/Debian | curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh |
| macOS | brew install --cask docker |
| Windows | قم بتنزيل Docker Desktop من docker.com |
| Linux Post-Install | sudo usermod -aG docker $USER && newgrp docker |
الأوامر الأساسية
عمليات دورة حياة الحاويات
| أمر | وصف |
|---|
container.start() | ابدأ مثيل حاوية |
container.stop() | توقف وأزل الحاوية |
container.isRunning() | تحقق مما إذا كان الحاوية قيد التشغيل حاليًا |
container.getContainerId() | احصل على معرف حاوية Docker |
container.getDockerImageName() | احصل على اسم الصورة المستخدمة |
container.getLogs() | استرجاع جميع سجلات الحاويات كسلسلة نصية |
container.getHost() | احصل على عنوان المضيف (عادةً localhost) |
container.getMappedPort(int) | احصل على منفذ المضيف المعين لمنفذ الحاوية |
container.getExposedPorts() | قم بسرد جميع منافذ الحاويات المكشوفة |
container.getContainerInfo() | احصل على معلومات مفصلة عن الحاويات |
إنشاء الحاويات العامة
| أمر | وصف |
|---|
new GenericContainer("image:tag") | أنشئ حاوية من أي صورة Docker |
.withExposedPorts(port1, port2) | قم بتعريض منافذ الحاوية للمضيف |
.withEnv("KEY", "value") | تعيين متغير البيئة |
.withCommand("cmd", "arg1") | تجاوز أمر الحاوية |
.withLabel("key", "value") | أضف تسمية Docker إلى الحاوية |
.withNetworkMode("host") | تعيين وضع شبكة الحاوية |
.withPrivilegedMode() | تشغيل الحاوية في الوضع المميز (privileged mode) |
.withCreateContainerCmdModifier(cmd) | قم بتعديل أمر إنشاء الحاوية |
.withStartupTimeout(Duration) | تعيين الحد الأقصى لوقت الانتظار عند بدء التشغيل |
.withReuse(true) | تمكين إعادة استخدام الحاويات عبر الاختبارات |
حاويات قواعد البيانات
| أمر | وصف |
|---|
new PostgreSQLContainer("postgres:15") | أنشئ حاوية PostgreSQL |
new MySQLContainer("mysql:8.0") | إنشاء حاوية MySQL |
new MongoDBContainer("mongo:6.0") | إنشاء حاوية MongoDB |
.withDatabaseName("dbname") | تعيين اسم قاعدة البيانات |
.withUsername("user") | تعيين اسم مستخدم قاعدة البيانات |
.withPassword("pass") | تعيين كلمة مرور قاعدة البيانات |
.getJdbcUrl() | احصل على عنوان URL للاتصال JDBC |
.getReplicaSetUrl() | احصل على عنوان URL لمجموعة النسخ المتكررة لـ MongoDB (MongoDB) |
.withInitScript("init.sql") | قم بتشغيل نص SQL عند بدء التشغيل |
.withConfigurationOverride("path") | تجاوز تكوين قاعدة البيانات |
الاستخدام المتقدم
استراتيجيات الانتظار
| أمر | وصف |
|---|
.waitingFor(Wait.forListeningPort()) | انتظر حتى يبدأ المنفذ (port) بالاستماع |
.waitingFor(Wait.forHttp("/health")) | انتظر استجابة نقطة النهاية HTTP |
.waitingFor(Wait.forLogMessage(".*ready.*", 1)) | انتظر رسالة سجل محددة |
.waitingFor(Wait.forHealthcheck()) | انتظر حتى يمر فحص صحة Docker |
.waitingFor(Wait.forSuccessfulCommand("cmd")) | انتظر حتى نجاح الأمر |
.forStatusCode(200) | حدد رمز الحالة HTTP المتوقع |
.forStatusCodeMatching(code -> code < 500) | مطابق رمز الحالة المخصص |
.withStartupTimeout(Duration.ofMinutes(2)) | تعيين مدة المهلة الزمنية للبدء |
.withReadTimeout(Duration.ofSeconds(10)) | ضبط مهلة القراءة HTTP |
.forResponsePredicate(response -> true) | التحقق المخصص من الاستجابة |
عمليات الملفات والمجلدات
| أمر | وصف |
|---|
.withFileSystemBind("host", "container") | ربط الدليل المضيف |
.withFileSystemBind(path, target, BindMode.READ_ONLY) | تركيب مع وضع محدد |
.withClasspathResourceMapping("res", "path") | قم بتحميل مورد المسار الفرعي |
.copyFileToContainer(MountableFile, "path") | نسخ الملف داخل الحاوية |
.withCopyFileToContainer(file, path) | نسخ الملف أثناء بدء التشغيل |
.withTmpFs(Map.of("/tmp", "rw")) | قم بتركيب نظام الملفات tmpfs |
MountableFile.forClasspathResource("file") | مرجع ملف classpath |
MountableFile.forHostPath("/path") | مسار نظام الملفات المضيف المرجعي |
BindMode.READ_WRITE | السماح بالوصول للقراءة والكتابة |
BindMode.READ_ONLY | السماح بالوصول للقراءة فقط |
الشبكات
| أمر | وصف |
|---|
Network.newNetwork() | إنشاء شبكة Docker مخصصة |
.withNetwork(network) | قم بإرفاق الحاوية بالشبكة |
.withNetworkAliases("alias") | قم بتعيين الاسم المستعار للشبكة للحاوية |
.dependsOn(otherContainer) | حدد اعتمادية الحاوية |
.withExtraHost("hostname", "ip") | أضف إدخال إلى /etc/hosts |
.getNetworkAliases() | احصل على جميع الأسماء المستعارة للشبكة |
.withAccessToHost(true) | السماح للحاوية بالوصول إلى المضيف |
Network.SHARED | استخدم الشبكة المشتركة عبر الاختبارات |
.withNetworkMode("bridge") | تعيين وضع شبكة محدد |
.getNetwork() | احصل على الحاوية الشبكية المرتبطة بها |
تنفيذ الحاويات
| أمر | وصف |
|---|
.execInContainer("cmd", "arg1") | نفذ الأمر في الحاوية قيد التشغيل |
.execInContainer(Charset, "cmd") | نفّذ باستخدام مجموعة أحرف محددة |
result.getStdout() | احصل على المخرجات القياسية من التنفيذ |
result.getStderr() | احصل على الخطأ القياسي من التنفيذ |
result.getExitCode() | احصل على كود خروج الأمر |
.copyFileFromContainer("path", consumer) | نسخ الملف من الحاوية |
.followOutput(consumer) | سجلات حاويات Stream |
new Slf4jLogConsumer(logger) | قم بتسجيل المخرجات في مسجل SLF4J |
new ToStringConsumer() | التقاط المخرجات كسلسلة |
new WaitingConsumer() | انتظر الإخراج المحدد |
تكامل JUnit 5
| أمر | وصف |
|---|
@Testcontainers | تمكين امتداد TestContainers |
@Container | حدد الحقل كحاوية للإدارة |
static @Container | مشاركة الحاوية عبر جميع الاختبارات |
@DynamicPropertySource | إدراج خصائص الحاوية |
registry.add("prop", container::getUrl) | أضف خاصية ديناميكية |
@BeforeAll | إعداد الحاويات قبل جميع الاختبارات |
@AfterAll | تنظيف الحاويات بعد جميع الاختبارات |
@Nested | قم بتجميع الاختبارات باستخدام حاويات مشتركة |
Startables.deepStart(containers).join() | ابدأ حاويات متعددة بشكل متوازٍ |
.withReuse(true) | إعادة استخدام الحاوية عبر تشغيلات الاختبار |
الإعدادات
خصائص TestContainers
إنشاء testcontainers.propertiesفي src/test/resources:
# Docker host configuration
docker.host=unix:///var/run/docker.sock
# docker.host=tcp://localhost:2375
# Container reuse (requires Docker labels support)
testcontainers.reuse.enable=true
# Image pull policy
testcontainers.image.pull.policy=DefaultPullPolicy
# Ryuk container (cleanup)
testcontainers.ryuk.disabled=false
testcontainers.ryuk.container.image=testcontainers/ryuk:0.5.1
# Checks
testcontainers.checks.disable=false
تكامل Docker Compose
@Container
static DockerComposeContainer environment =
new DockerComposeContainer(new File("docker-compose.yml"))
.withExposedService("db", 5432)
.withExposedService("redis", 6379)
.withLocalCompose(true)
.withPull(true)
.waitingFor("db", Wait.forHealthcheck());
إعداد الحاويات المخصصة
public class CustomPostgreSQLContainer extends PostgreSQLContainer<CustomPostgreSQLContainer> {
private static final String IMAGE_VERSION = "postgres:15-alpine";
public CustomPostgreSQLContainer() {
super(IMAGE_VERSION);
this.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass")
.withInitScript("init.sql")
.withCommand("postgres -c max_connections=200");
}
}
تكامل Spring Boot
@SpringBootTest
@Testcontainers
class ApplicationTests {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop");
}
@Test
void contextLoads() {
// Test with real database
}
}
حالات الاستخدام الشائعة
حالة الاستخدام 1: اختبار التكامل PostgreSQL
Note: Since some of the texts were empty or very short, I’ve provided minimal translations that maintain the structure. For texts with no content, I’ve left them as placeholders.```java
@Testcontainers
@SpringBootTest
class UserRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
.withInitScript("schema.sql");
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndFindUser() {
User user = new User("john@example.com", "John Doe");
userRepository.save(user);
Optional<User> found = userRepository.findByEmail("john@example.com");
assertTrue(found.isPresent());
assertEquals("John Doe", found.get().getName());
}
}
```java
@Testcontainers
class KafkaIntegrationTest {
@Container
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.5.0")
);
@DynamicPropertySource
static void kafkaProperties(DynamicPropertyRegistry registry) {
registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
}
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Autowired
private MessageListener messageListener;
@Test
void shouldProcessMessage() throws Exception {
String topic = "test-topic";
String message = "Hello Kafka!";
kafkaTemplate.send(topic, message);
// Wait for message processing
await().atMost(Duration.ofSeconds(10))
.until(() -> messageListener.getReceivedMessages().size() == 1);
assertEquals(message, messageListener.getReceivedMessages().get(0));
}
}
```### حالة الاستخدام 3: اختبار الخدمات الدقيقة متعددة الحاويات
```java
@Testcontainers
class MicroservicesIntegrationTest {
static Network network = Network.newNetwork();
@Container
static PostgreSQLContainer<?> database = new PostgreSQLContainer<>("postgres:15-alpine")
.withNetwork(network)
.withNetworkAliases("database")
.withDatabaseName("orders");
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withNetwork(network)
.withNetworkAliases("redis")
.withExposedPorts(6379);
@Container
static GenericContainer<?> orderService = new GenericContainer<>("order-service:latest")
.withNetwork(network)
.withExposedPorts(8080)
.withEnv("DATABASE_URL", "jdbc:postgresql://database:5432/orders")
.withEnv("REDIS_HOST", "redis")
.dependsOn(database, redis)
.waitingFor(Wait.forHttp("/actuator/health").forStatusCode(200));
@Test
void shouldCreateOrder() {
String baseUrl = "http://" + orderService.getHost() + ":"
+ orderService.getMappedPort(8080);
// Make HTTP request to order service
RestTemplate restTemplate = new RestTemplate();
Order order = new Order("item-123", 2);
ResponseEntity<Order> response = restTemplate.postForEntity(
baseUrl + "/orders", order, Order.class
);
assertEquals(HttpStatus.CREATED, response.getStatusCode());
assertNotNull(response.getBody().getId());
}
}
```### حالة الاستخدام 4: اختبار تكامل MongoDB
```java
@Testcontainers
@DataMongoTest
class ProductRepositoryTest {
@Container
static MongoDBContainer mongodb = new MongoDBContainer("mongo:6.0")
.withExposedPorts(27017);
@DynamicPropertySource
static void mongoProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.mongodb.uri", mongodb::getReplicaSetUrl);
}
@Autowired
private ProductRepository productRepository;
@Test
void shouldFindProductsByCategory() {
// Insert test data
productRepository.save(new Product("Laptop", "Electronics", 999.99));
productRepository.save(new Product("Mouse", "Electronics", 29.99));
productRepository.save(new Product("Desk", "Furniture", 299.99));
// Query by category
List<Product> electronics = productRepository.findByCategory("Electronics");
assertEquals(2, electronics.size());
assertTrue(electronics.stream()
.allMatch(p -> p.getCategory().equals("Electronics")));
}
}
```### حالة الاستخدام 5: بيئة اختبار Docker Compose
```java
@Testcontainers
class FullStackIntegrationTest {
@Container
static DockerComposeContainer<?> environment = new DockerComposeContainer<>(
new File("src/test/resources/docker-compose-test.yml")
)
.withExposedService("api", 8080,
Wait.forHttp("/health").forStatusCode(200))
.withExposedService("postgres", 5432,
Wait.forListeningPort())
.withExposedService("redis", 6379)
.withLocalCompose(true);
@Test
void shouldConnectToAllServices() {
String apiHost = environment.getServiceHost("api", 8080);
Integer apiPort = environment.getServicePort("api", 8080);
String apiUrl = "http://" + apiHost + ":" + apiPort;
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.getForEntity(
apiUrl + "/health", String.class
);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
}
```## أفضل الممارسات
### إدارة الحاويات
- **استخدام حاويات ثابتة لفئات الاختبار**: مشاركة الحاويات عبر جميع طرق الاختبار لتقليل وقت التشغيل واستخدام الموارد
- **تنفيذ إعادة استخدام الحاويات**: تمكين`testcontainers.reuse.enable=true`لدورات اختبار التطوير المحلي الأسرع
- **تنظيف الموارد**: استخدام`@AfterAll`أو try-with-resources عند إدارة الحاويات يدويًا خارج تكامل JUnit
- **استخدام علامات صور محددة**: تجنب`latest`؛ استخدم إصدارات محددة مثل`postgres:15-alpine`للاختبارات القابلة للتكرار
### تحسين الأداء
- **بدء تشغيل الحاويات بشكل متوازٍ**: استخدم`Startables.deepStart()`لبدء تشغيل حاويات متعددة مستقلة في وقت واحد
- **تقليل إعادة تشغيل الحاويات**: استخدم`@Container`على مستوى الفئة بدلاً من مستوى الطريقة عند الإمكان
- **اختيار صور خفيفة**: فضّل الصور المعتمدة على Alpine (مثل`postgres:15-alpine`) لتقليل وقت السحب واستخدام القرص
- **تنفيذ استراتيجيات الانتظار بحكمة**: استخدم استراتيجيات انتظار مناسبة لتجنب التأخيرات غير الضرورية مع ضمان جاهزية الحاوية
### استراتيجية الاختبار
- **الاختبار مقابل التبعيات المشابهة للإنتاج**: استخدم محركات قواعد البيانات ووسطاء الرسائل الحقيقية بدلاً من النسخ المضمنة
- **عزل بيانات الاختبار**: تأكد من أن كل اختبار ينشئ وينظف بياناته الخاصة لمنع الترابط بين الاختبارات
- **استخدام أسماء الشبكة**: إنشاء شبكات مخصصة واستخدام أسماء ذات معنى للتواصل بين الحاويات
- **تنفيذ فحوصات الصحة**: تأكد دائمًا من تكوين استراتيجيات انتظار مناسبة لضمان استعداد الحاويات بالكامل قبل تنفيذ الاختبارات
### تكامل CI/CD
- **التحقق من توفر Docker**: تأكد من تثبيت Docker وإمكانية الوصول إليه في بيئة CI
- **تكوين مهل زمنية بشكل مناسب**: ضع مهلًا زمنية واقعية مع مراعاة أداء بيئة CI
- **استخدام ذاكرة التخزين المؤقت للصور**: تكوين CI لتخزين صور Docker مؤقتًا لتسريع التشغيلات اللاحقة
- **مراقبة استخدام الموارد**: كن على دراية بحدود الذاكرة ووحدة المعالجة المركزية في بيئات CI؛ اضبط تكوينات الحاويات وفقًا لذلك
### الأمان والصيانة
- **الحفاظ على تحديث TestContainers**: قم بالتحديث بانتظام إلى أحدث الإصدارات للتصحيحات الأمنية والميزات الجديدة
- **استخدام الصور الرسمية**: فضّل صور Docker الرسمية من الناشرين الموثوقين
- **تجنب الوضع المميز**: استخدم`.withPrivilegedMode()`فقط عند الضرورة القصوى
- **مراجعة المنافذ المكشوفة**: كشف المنافذ المطلوبة فقط للاختبار
## استكشاف الأخطاء وإصلاحها
| مشكلة | حل |
|-------|----------|
| **Container fails to start: "Cannot connect to Docker daemon"** | Ensure Docker is running: `docker ps`. On Linux, add user to docker group: `sudo usermod -aG docker $USER` |
| **Tests timeout during container startup** | Increase timeout: `.withStartupTimeout(Duration.ofMinutes(5))` or check Docker resources (CPU/Memory) |
| **Port binding conflicts: "Address already in use"** | Use dynamic ports: `container.getMappedPort()` instead of fixed ports, or stop conflicting services |
| **Image pull fails: "manifest unknown"** | Verify image name and tag exist on Docker Hub. Use specific tags instead of `latest` |
| **Container starts but tests fail to connect** | Check wait strategy is appropriate: add `.waitingFor(Wait.forListeningPort())` or `.waitingFor(Wait.forHealthcheck())` |
| **Out of memory errors during tests** | Reduce number of parallel containers, increase Docker memory limit in Docker Desktop settings, or use `.withReuse(true)` |
| **"Ryuk container not found" warnings** | Disable Ryuk if needed: set `testcontainers.ryuk.disabled=true` in properties (not recommended for production) |
| **Tests work locally but fail in CI** | تأكد من تثبيت Docker في CI، تحقق من إعدادات المهلة الزمنية، تحقق من اتصال الشبكة، راجع حدود موارد بيئة CI |
| **Database connection refused** | Wait for database to be ready: use `.waitingFor(Wait.forListeningPort())` and verify JDBC URL with `container.getJdbcUrl()` |
| **Container logs show errors but tests pass** | Enable log following: `container.followOutput(new Slf4jLogConsumer(log))` to debug container issues |
| **Slow test execution** | Use container reuse, share containers at class level, start containers in parallel with `Startables.deepStart()` |
| **"No such file or directory" when mounting volumes** | Use absolute paths or `MountableFile.forClasspathResource()` for classpath resources |
| **Network communication between containers fails** | Create custom network: `Network.newNetwork()` and attach containers with `.withNetwork(network)` and `.withNetworkAliases()` |
| **TestContainers properties not being read** | Ensure `testcontainers.properties` is in `src/test/resources` and properly formatted |
| **Container cleanup not happening** | Check Ryuk container is running: `docker ps | grep ryuk`. Ensure tests complete normally without hanging |
## الوحدات المتاحة
| وحدة | مجموعة Maven | وصف |
|--------|---------------|-------------|
| PostgreSQL | `testcontainers-postgresql` | حاويات قاعدة بيانات PostgreSQL |
| MySQL | `testcontainers-mysql` | حاويات قاعدة بيانات MySQL |
| MongoDB | `testcontainers-mongodb` | قاعدة بيانات MongoDB الوثائقية |
| Kafka | `testcontainers-kafka` | وسيط رسائل Apache Kafka |
| Redis | `testcontainers-redis` | متجر بيانات Redis في الذاكرة |
| Elasticsearch | `testcontainers-elasticsearch` | محرك بحث Elasticsearch |
| Cassandra | `testcontainers-cassandra` | قاعدة بيانات Cassandra اللامركزية |
| RabbitMQ | `testcontainers-rabbitmq` | وسيط الرسائل RabbitMQ |
| Selenium | `testcontainers-selenium` | حاويات أتمتة المتصفح |
| LocalStack | `testcontainers-localstack` | محاكاة خدمات AWS || Nginx