انتقل إلى المحتوى الرئيسي

الفصل 5: اختبارات REST API

مقدمة​

يجب أن تستجيب REST API الخاصة بك بشكل صحيح لطلبات HTTP. يتيح لك MockMvc اختبار نقاط النهاية الخاصة بك دون تشغيل خادم ويب حقيقي.


🎯 أهداف الفصل​

في نهاية هذا الفصل، ستتمكن من:

  1. ✅ استخدام MockMvc لاختبار نقاط النهاية
  2. ✅ اختبار GET، POST، PUT، DELETE
  3. ✅ التحقق من صحة الردود JSON
  4. ✅ اختبار الأخطاء والاستثناءات
  5. ✅ اختبار مع المصادقة

1. إعداد MockMvc​

مع @WebMvcTest​

@WebMvcTest(ProductController.class)
class ProductControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private ProductService productService;

@Test
void shouldReturnProducts() throws Exception {
mockMvc.perform(get("/api/products"))
.andExpect(status().isOk());
}
}

مع @SpringBootTest​

@SpringBootTest
@AutoConfigureMockMvc // ← Configure MockMvc
class FullIntegrationTest {

@Autowired
private MockMvc mockMvc;

@Test
void testWithRealServices() throws Exception {
mockMvc.perform(get("/api/products"))
.andExpect(status().isOk());
}
}

2. اختبار GET​

GET بسيط​

@Test
void shouldReturnAllProducts() throws Exception {
when(productService.findAll()).thenReturn(List.of(
new Product(1L, "Laptop", 1000.0),
new Product(2L, "Phone", 500.0)
));

mockMvc.perform(get("/api/products"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$.length()").value(2))
.andExpect(jsonPath("$[0].name").value("Laptop"))
.andExpect(jsonPath("$[1].price").value(500.0));
}

GET مع Path Variable​

@Test
void shouldReturnProductById() throws Exception {
when(productService.findById(1L))
.thenReturn(Optional.of(new Product(1L, "Laptop", 1000.0)));

mockMvc.perform(get("/api/products/{id}", 1))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Laptop"));
}

GET مع Query Parameters​

@Test
void shouldSearchProducts() throws Exception {
when(productService.search("laptop", 500.0, 1500.0))
.thenReturn(List.of(new Product(1L, "Laptop", 1000.0)));

mockMvc.perform(get("/api/products/search")
.param("query", "laptop")
.param("minPrice", "500")
.param("maxPrice", "1500"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(1));
}

3. اختبار POST​

POST مع JSON Body​

@Test
void shouldCreateProduct() throws Exception {
Product newProduct = new Product(null, "Tablet", 600.0);
Product savedProduct = new Product(1L, "Tablet", 600.0);

when(productService.save(any(Product.class))).thenReturn(savedProduct);

mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "Tablet",
"price": 600.0
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Tablet"));
}

مع ObjectMapper​

@Autowired
private ObjectMapper objectMapper;

@Test
void shouldCreateProductWithObjectMapper() throws Exception {
Product product = new Product(null, "Tablet", 600.0);

when(productService.save(any())).thenReturn(new Product(1L, "Tablet", 600.0));

mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(product)))
.andExpect(status().isCreated());
}

4. اختبار PUT​

@Test
void shouldUpdateProduct() throws Exception {
Product updated = new Product(1L, "Updated Laptop", 1200.0);

when(productService.update(eq(1L), any())).thenReturn(updated);

mockMvc.perform(put("/api/products/{id}", 1)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "Updated Laptop",
"price": 1200.0
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Updated Laptop"))
.andExpect(jsonPath("$.price").value(1200.0));
}

5. اختبار DELETE​

@Test
void shouldDeleteProduct() throws Exception {
doNothing().when(productService).delete(1L);

mockMvc.perform(delete("/api/products/{id}", 1))
.andExpect(status().isNoContent());

verify(productService).delete(1L);
}

6. اختبار الأخطاء​

404 Not Found​

@Test
void shouldReturn404WhenProductNotFound() throws Exception {
when(productService.findById(999L))
.thenThrow(new ProductNotFoundException(999L));

mockMvc.perform(get("/api/products/{id}", 999))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("Product not found: 999"));
}

400 Bad Request (Validation)​

@Test
void shouldReturn400WhenValidationFails() throws Exception {
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "",
"price": -100
}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors[*].field").value(hasItem("name")));
}

500 Internal Server Error​

@Test
void shouldReturn500OnServerError() throws Exception {
when(productService.findAll())
.thenThrow(new RuntimeException("Database connection failed"));

mockMvc.perform(get("/api/products"))
.andExpect(status().isInternalServerError());
}

7. تعبيرات JSONPath​

الصيغة الشائعة​

// قيمة بسيطة
jsonPath("$.name").value("Laptop")

// عنصر في مصفوفة
jsonPath("$[0].name").value("First")
jsonPath("$[1].price").value(100.0)

// حجم المصفوفة
jsonPath("$.length()").value(5)
jsonPath("$").isArray()

// الوجود
jsonPath("$.id").exists()
jsonPath("$.deleted").doesNotExist()

// النوع
jsonPath("$.price").isNumber()
jsonPath("$.name").isString()
jsonPath("$.items").isArray()

// المقارنة
jsonPath("$.price").value(greaterThan(100.0)))
jsonPath("$.items.length()").value(lessThanOrEqualTo(10)))

// Wildcard
jsonPath("$[*].name").value(hasItem("Laptop"))
jsonPath("$.items[*].price").value(everyItem(greaterThan(0.0))))

8. اختبار مع المصادقة​

بدون Security (Mock)​

@WebMvcTest(ProductController.class)
@AutoConfigureMockMvc(addFilters = false) // ← Désactive les filtres Security
class ProductControllerNoSecurityTest {
// Tests sans authentification
}

مع @WithMockUser​

@WebMvcTest(ProductController.class)
class SecuredControllerTest {

@Autowired
private MockMvc mockMvc;

@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void adminCanAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/products"))
.andExpect(status().isOk());
}

@Test
@WithMockUser(username = "user", roles = {"USER"})
void userCannotAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/products"))
.andExpect(status().isForbidden());
}

@Test
void anonymousCannotAccess() throws Exception {
mockMvc.perform(get("/api/products"))
.andExpect(status().isUnauthorized());
}
}

مع JWT​

@Test
void shouldAccessWithValidToken() throws Exception {
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";

mockMvc.perform(get("/api/products")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}

9. تصحيح الأخطاء وتسجيلها​

andDo(print())​

@Test
void debugTest() throws Exception {
mockMvc.perform(get("/api/products"))
.andDo(print()) // ← Affiche la requête et réponse complètes
.andExpect(status().isOk());
}

مثال على المخرجات​

MockHttpServletRequest:
HTTP Method = GET
Request URI = /api/products
Parameters = {}
Headers = []

MockHttpServletResponse:
Status = 200
Content type = application/json
Body = [{"id":1,"name":"Laptop","price":1000.0}]

10. مثال كامل CRUD​

@WebMvcTest(ProductController.class)
class ProductControllerTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;

@MockBean
private ProductService productService;

// CREATE
@Test
void shouldCreateProduct() throws Exception {
when(productService.save(any())).thenReturn(new Product(1L, "Laptop", 1000.0));

mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Laptop\",\"price\":1000.0}"))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.id").value(1));
}

// READ ALL
@Test
void shouldReturnAllProducts() throws Exception {
when(productService.findAll()).thenReturn(List.of(
new Product(1L, "Laptop", 1000.0),
new Product(2L, "Phone", 500.0)
));

mockMvc.perform(get("/api/products"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2));
}

// READ ONE
@Test
void shouldReturnProductById() throws Exception {
when(productService.findById(1L))
.thenReturn(Optional.of(new Product(1L, "Laptop", 1000.0)));

mockMvc.perform(get("/api/products/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Laptop"));
}

// UPDATE
@Test
void shouldUpdateProduct() throws Exception {
when(productService.update(eq(1L), any()))
.thenReturn(new Product(1L, "Updated", 1500.0));

mockMvc.perform(put("/api/products/1")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Updated\",\"price\":1500.0}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Updated"));
}

// DELETE
@Test
void shouldDeleteProduct() throws Exception {
mockMvc.perform(delete("/api/products/1"))
.andExpect(status().isNoContent());

verify(productService).delete(1L);
}

// ERROR - Not Found
@Test
void shouldReturn404() throws Exception {
when(productService.findById(999L)).thenReturn(Optional.empty());

mockMvc.perform(get("/api/products/999"))
.andExpect(status().isNotFound());
}

// ERROR - Validation
@Test
void shouldReturn400OnInvalidData() throws Exception {
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"\",\"price\":-1}"))
.andExpect(status().isBadRequest());
}
}

11. ملخص​

طرق MockMvc​

الطريقةالاستخدام
get()طلب GET
post()طلب POST
put()طلب PUT
delete()طلب DELETE
patch()طلب PATCH

التأكيدات الشائعة​

.andExpect(status().isOk())              // 200
.andExpect(status().isCreated()) // 201
.andExpect(status().isNoContent()) // 204
.andExpect(status().isBadRequest()) // 400
.andExpect(status().isUnauthorized()) // 401
.andExpect(status().isForbidden()) // 403
.andExpect(status().isNotFound()) // 404
.andExpect(status().isInternalServerError()) // 500

.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.field").value(expected))
.andExpect(header().exists("Location"))

🎯 الخطوة التالية​

حان وقت التطبيق العملي! ستساعدك التدريبات العملية على إتقان الاختبارات.

➡️ TP9.1: اختبارات وحدات - طبقة الخدمة


⬅️ العودة إلى الوحدة 9