بالدرس السابق عملنا Unit Tests. اليوم رح نعمل Integration Tests - نختبر الـ Flow الكامل من الـ Controller للـ Database وارجع.
| Unit Test | Integration Test |
|---|---|
| يختبر وحدة واحدة معزولة | يختبر الـ Flow الكامل |
| يستخدم Mocks | يستخدم Database حقيقية |
| سريع جداً | أبطأ لكن أشمل |
| ما يحتاج بيئة خاصة | يحتاج Docker |
بدل ما نحكي مع قاعدة البيانات الحقيقية للـ Production، TestContainers بتستخدم Docker لتشغيل قاعدة بيانات مؤقتة خاصة بالتستات. بعد انتهاء الـ Tests، بتسكر وبتحذف قاعدة البيانات تلقائياً.
لازم يكون Docker Desktop مثبت على جهازكم.
بالـ pom.xml نضيف الـ Dependencies التالية:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
لأن TestContainers بتنشئ Database فارغة، لازم نعطيها الـ Schema. بنحول الـ Entities لـ SQL باستخدام ChatGPT:
بنحفظ الملف بـ src/test/resources/schema.sql:
CREATE TABLE IF NOT
اشترك في النشرة البريدية
دروس جديدة، مقالات، وأدوات مباشرة لبريدك.
وبالـ application.properties:
spring.sql.init.mode=always@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class EmployeeIntegrationTest {
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@LocalServerPort
private Integer port;
@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);
}
@BeforeAll
static void startContainer() {
postgres.start();
}
@AfterAll
static void stopContainer() {
postgres.stop();
}
}RANDOM_PORT - يشغل السيرفر على Port عشوائي لتجنب التعارض@DynamicPropertySource - يعطي TestContainers معلومات الاتصال للـ Springpostgres متغير static لأن الـ Container يشتغل مرة وحدة لكل الـ Testsلازم نبدأ كل Test Case ببيانات نظيفة:
@Autowired
private EmployeeRepository employeeRepository;
@Autowired
private DepartmentRepository departmentRepository;
@Autowired
private UserAccountRepository userAccountRepository;
private Department department;
@BeforeEach
void setup() {
// الترتيب مهم - نحذف الـ Child قبل الـ Parent
userAccountRepository.deleteAll();
employeeRepository.deleteAll();
departmentRepository.deleteAll();
department = departmentRepository.save(
new Department(null, "IT")
);
}الترتيب بالحذف مهم بسبب الـ Foreign Key Constraints.
@Test
void shouldGetAllEmployeesAsAdmin() {
// Arrange
Employee employee = createEmployee("John", "Doe", "john@example.com");
UserAccount admin = createAdminAccount("admin", "password", employee);
// Login لأخذ الـ Token
String token = RestAssured.given()
.port(port)
.contentType("application/json")
.body("""
{"username": "admin", "password": "password"}
""")
.when()
.post("/auth/login")
.then()
.statusCode(201)
.extract()
.path("data");
// استخدام الـ Token لجلب الموظفين
RestAssured.given()
.port(port)
.header("Authorization", "Bearer " + token)
.when()
.get("/employees")
.then()
.statusCode(200)
.body("data.content.size()", equalTo(1));
}@Test
void shouldNotGetAllEmployeesAsUser() {
// نفس الـ Flow بس مع User عادي مش Admin
String userToken = loginAs("regular-user");
RestAssured.given()
.port(port)
.header("Authorization", "Bearer " + userToken)
.when()
.get("/employees")
.then()
.statusCode(403);
}بنعمل دوال مساعدة لتقليل تكرار الكود:
private Employee createEmployee(String firstName, String lastName, String email) {
Employee employee = new Employee();
employee.setFirstName(firstName);
employee.setLastName(lastName);
employee.setEmail(email);
employee.setDepartment(department);
return employeeRepository.save(employee);
}لا تعملوا Integration Test للـ Email Service أو الـ Payment Gateway - اعملوا لهم Mock. قاعدة البيانات مناسبة تماماً للـ Integration Tests لأن TestContainers بتسهّل عملية إنشاء بيئة نظيفة.
التستات رح تأخذ بضع ثواني لتشغيل Docker Container - هذا طبيعي. بعد الانتهاء، Docker رح يسكر والـ Database رح تختفي.
الـ Integration Tests بتكمل الـ Unit Tests وليست بديلاً عنها. الاثنين مع بعض بيعطونكم ثقة أكبر بالكود.