الفصل 4: اختبارات التكامل
مقدمة
الاختبارات الوحدوية مع الموكات سريعة ولكنها لا تضمن أن المكونات تعمل معًا. اختبارات التكامل تتحقق من أن Spring وJPA وقاعدة البيانات الخاصة بك تتعاون بشكل صحيح.
🎯 أهداف الفصل
في نهاية هذا الفصل، ستعرف:
- ✅ استخدام @SpringBootTest
- ✅ اختبار المستودعات مع @DataJpaTest
- ✅ اختبار المتحكمات مع @WebMvcTest
- ✅ تكوين ملفات تعريف الاختبار
- ✅ استخدام TestContainers (إضافي)
1. @SpringBootTest
اختبار تكامل كامل
@SpringBootTest // ← يشحن السياق الكامل لـ Spring
class ApplicationIntegrationTest {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Test
void contextLoads() {
// يتحقق من أن التطبيق يبدأ
assertNotNull(userService);
}
@Test
void shouldCreateAndFindUser() {
User user = userService.create("John", "john@test.com");
User found = userService.findById(user.getId());
assertEquals("John", found.getName());
}
}
خيارات @SpringBootTest
// سياق كامل مع خادم ويب
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class FullStackTest {
@LocalServerPort
private int port;
}
// بدون خادم ويب (أسرع)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class ServiceTest { }
// مع تكوين محدد
@SpringBootTest(properties = {
"spring.datasource.url=jdbc:h2:mem:testdb",
"logging.level.root=WARN"
})
class ConfiguredTest { }
2. @DataJpaTest
اختبار المستودع
@DataJpaTest // ← يكوّن فقط JPA + H2
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindByEmail() {
// ترتيب
User user = new User("John", "john@test.com");
entityManager.persistAndFlush(user);
// تنفيذ
Optional<User> found = userRepository.findByEmail("john@test.com");
// تحقق
assertTrue(found.isPresent());
assertEquals("John", found.get().getName());
}
@Test
void shouldReturnEmptyWhenEmailNotFound() {
Optional<User> found = userRepository.findByEmail("unknown@test.com");
assertFalse(found.isPresent());
}
}
ما يكوّنه @DataJpaTest
@DataJpaTest يشمل:
├── H2 في الذاكرة (افتراضيًا)
├── Hibernate/JPA
├── TestEntityManager
├── المستودعات
└── @Transactional (إرجاع بعد كل اختبار)
لا يشمل:
├── @Service
├── @Controller
├── Spring Security
└── مكونات أخرى
اختبار الطلبات المخصصة
@DataJpaTest
class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Autowired
private TestEntityManager em;
@BeforeEach
void setUp() {
em.persist(new Product("Laptop", 1000.0, "Electronics"));
em.persist(new Product("Phone", 800.0, "Electronics"));
em.persist(new Product("Shirt", 50.0, "Clothing"));
em.flush();
}
@Test
void shouldFindByCategory() {
List<Product> electronics = productRepository.findByCategory("Electronics");
assertThat(electronics).hasSize(2);
}
@Test
void shouldFindByPriceRange() {
List<Product> products = productRepository.findByPriceBetween(100.0, 900.0);
assertThat(products)
.hasSize(1)
.extracting(Product::getName)
.containsExactly("Phone");
}
@Test
void shouldCountByCategory() {
long count = productRepository.countByCategory("Electronics");
assertEquals(2, count);
}
}
3. @WebMvcTest
اختبار المتحكم (بدون خدمة حقيقية)
@WebMvcTest(UserController.class) // ← يختبر هذا المتحكم فقط
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean // ← موك الخدمة
private UserService userService;
@Test
void shouldReturnUser() throws Exception {
// ترتيب
when(userService.findById(1L))
.thenReturn(new User(1L, "John", "john@test.com"));
// تنفيذ وتحقق
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"))
.andExpect(jsonPath("$.email").value("john@test.com"));
}
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
when(userService.findById(anyLong()))
.thenThrow(new UserNotFoundException(999L));
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
}
@MockBean مقابل @Mock
// @MockBean: للاختبارات في Spring (يتم حقنه في السياق)
@WebMvcTest
class ControllerTest {
@MockBean
private UserService userService; // يستبدل bean في Spring
}
// @Mock: للاختبارات الوحدوية البحتة (Mockito فقط)
@ExtendWith(MockitoExtension.class)
class ServiceTest {
@Mock
private UserRepository userRepository; // موك بسيط من Mockito
}
4. ملفات تعريف الاختبار
application-test.properties
# src/test/resources/application-test.properties
# قاعدة بيانات H2
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
# تعطيل الميزات
spring.flyway.enabled=false
spring.mail.host=localhost
تفعيل ملف التعريف
@SpringBootTest
@ActiveProfiles("test") // ← يستخدم application-test.properties
class MyIntegrationTest {
// ...
}
التعليق @TestPropertySource
@SpringBootTest
@TestPropertySource(properties = {
"spring.jpa.show-sql=false",
"app.feature.enabled=true"
})
class OverriddenPropertiesTest {
// ...
}
5. @Transactional في الاختبارات
إرجاع تلقائي
@SpringBootTest
@Transactional // ← إرجاع بعد كل اختبار
class TransactionalTest {
@Autowired
private UserRepository userRepository;
@Test
void test1() {
userRepository.save(new User("John"));
assertEquals(1, userRepository.count());
}
@Test
void test2() {
// قاعدة البيانات فارغة لأن test1 تم إرجاعه
assertEquals(0, userRepository.count());
}
}
@DataJpaTest هو @Transactional افتراضيًا
@DataJpaTest // ← بالفعل @Transactional
class RepositoryTest {
// إرجاع تلقائي بعد كل اختبار
}
6. اختبار مع بيانات أولية
@Sql
@SpringBootTest
@Sql("/test-data.sql") // ← ينفذ قبل كل اختبار
class SqlDataTest {
// ...
}
@Test
@Sql(scripts = "/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testWithCleanup() {
// ...
}
@SqlGroup
@SpringBootTest
@SqlGroup({
@Sql("/schema.sql"),
@Sql("/test-data.sql")
})
class MultiSqlTest {
// ...
}
ملف test-data.sql
-- src/test/resources/test-data.sql
INSERT INTO users (id, name, email) VALUES (1, 'John', 'john@test.com');
INSERT INTO users (id, name, email) VALUES (2, 'Jane', 'jane@test.com');
INSERT INTO products (id, name, price) VALUES (1, 'Laptop', 1000.00);
7. TestContainers (إضافي)
لماذا؟
H2 مريح ولكنه مختلف عن PostgreSQL/MySQL. TestContainers يشغل قواعد بيانات حقيقية في Docker.
التكوين
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
الاستخدام
@SpringBootTest
@Testcontainers
class PostgreSQLIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@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);
}
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveToRealPostgres() {
User user = userRepository.save(new User("John", "john@test.com"));
assertNotNull(user.getId());
}
}
8. شرائح اختبار Spring Boot
ملخص
| Annotation | السياق المحمل | الاستخدام |
|---|---|---|
@SpringBootTest | الكل | اختبارات التكامل الكاملة |
@DataJpaTest | JPA + H2 | اختبارات المستودع |
@WebMvcTest | MVC + Security | اختبارات المتحكم |
@JsonTest | Jackson | اختبارات التسلسل |
@RestClientTest | RestTemplate | اختبارات العملاء REST |
الجمع مع @MockBean
@DataJpaTest
@Import(UserService.class) // استيراد الخدمة
class UserServiceWithRealDbTest {
@Autowired
private UserService userService;
@MockBean
private EmailService emailService; // موك خارجي
@Test
void shouldSaveUser() {
// userService يستخدم المستودع الحقيقي
// emailService موك
}
}
9. ملخص
التعليقات الأساسية
| Annotation | الاستخدام |
|---|---|
@SpringBootTest | السياق الكامل |
@DataJpaTest | اختبارات JPA/المستودع |
@WebMvcTest | اختبارات المتحكم |
@MockBean | موك في سياق Spring |
@ActiveProfiles | ملف تعريف الاختبار |
@Sql | بيانات SQL |
@Transactional | إرجاع تلقائي |
اختيار نوع الاختبار
┌─────────────────────────────────────────────┐
│ ماذا تختبر؟ │
├──────────────────────┬──────────────────────┤
│ المستودع │ @DataJpaTest │
│ المتحكم فقط │ @WebMvcTest │
│ الخدمة مع الموكات │ @ExtendWith(Mockito) │
│ المكدس الكامل │ @SpringBootTest │
│ مع قاعدة بيانات حقيقية│ TestContainers │
└──────────────────────┴──────────────────────┘
🎯 الخطوة التالية
أنت تعرف كيفية اختبار المستودعات والخدمات. الآن، دعونا نتعلم كيفية اختبار API REST بالتفصيل مع MockMvc.