@WebMvcTest ve MockMvc
@WebMvcTest, Spring Boot'un slice test konseptinin web katmanı için olan versiyonudur. Tam application context yerine sadece web katmanını (controller, filter, advice, converter) ayağa kaldırır. Service ve repository bean'leri yüklenmez — @MockBean ile mock'lanır. Bu sayede controller testleri çok hızlı çalışır.
@WebMvcTest Temelleri
@WebMvcTest(UserController.class) // Sadece bu controller'ı yükle
class UserControllerTest {
@Autowired
private MockMvc mockMvc; // HTTP istekleri simüle eder
@MockBean
private UserService userService; // Service mock'lanır
@Test
@DisplayName("GET /api/users/{id} — başarılı yanıt")
void shouldReturnUser() throws Exception {
UserDto user = new UserDto(1L, "Ali", "ali@test.com");
when(userService.getUserById(1L)).thenReturn(user);
mockMvc.perform(get("/api/users/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Ali"))
.andExpect(jsonPath("$.email").value("ali@test.com"));
}
}@WebMvcTest sadece şunları yükler:
@Controller,@RestController@ControllerAdvice@JsonComponentFilter,WebMvcConfigurerHandlerMethodArgumentResolver
Yüklemedikleri: @Service, @Repository, @Component
MockMvc — HTTP İstek Simülasyonu
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductService productService;
// ─── GET Testleri ──────────────────────────────────────
@Test
void shouldReturnAllProducts() throws Exception {
List<ProductDto> products = List.of(
new ProductDto(1L, "Laptop", 999.99),
new ProductDto(2L, "Mouse", 29.99)
);
when(productService.findAll()).thenReturn(products);
mockMvc.perform(get("/api/products"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$.length()").value(2))
.andExpect(jsonPath("$[0].name").value("Laptop"))
.andExpect(jsonPath("$[1].price").value(29.99));
}
@Test
void shouldReturn404WhenProductNotFound() throws Exception {
when(productService.findById(999L))
.thenThrow(new ProductNotFoundException("Not found"));
mockMvc.perform(get("/api/products/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("Not found"));
}
// ─── POST Testleri ─────────────────────────────────────
@Test
void shouldCreateProduct() throws Exception {
ProductDto created = new ProductDto(1L, "Laptop", 999.99);
when(productService.create(any())).thenReturn(created);
String requestBody = """
{
"name": "Laptop",
"price": 999.99
}
""";
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Laptop"))
.andExpect(header().exists("Location"));
}
@Test
void shouldReturn400ForInvalidProduct() throws Exception {
// @Valid ile doğrulama — name boş olamaz
String invalidBody = """
{
"name": "",
"price": -5
}
""";
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidBody))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors.length()").value(
greaterThanOrEqualTo(1)));
}
// ─── PUT Testleri ──────────────────────────────────────
@Test
void shouldUpdateProduct() throws Exception {
ProductDto updated = new ProductDto(1L, "Gaming Laptop", 1499.99);
when(productService.update(eq(1L), any())).thenReturn(updated);
mockMvc.perform(put("/api/products/1")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "Gaming Laptop", "price": 1499.99}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Gaming Laptop"));
}
// ─── DELETE Testleri ───────────────────────────────────
@Test
void shouldDeleteProduct() throws Exception {
doNothing().when(productService).delete(1L);
mockMvc.perform(delete("/api/products/1"))
.andExpect(status().isNoContent());
verify(productService).delete(1L);
}
}jsonPath ile Detaylı Doğrulama
JsonPath, JSON yanıtının belirli alanlarını sorgulamak için XPath benzeri bir dil kullanır:
// Temel erişim
.andExpect(jsonPath("$.name").value("Ali"))
// Array elemanları
.andExpect(jsonPath("$[0].name").value("Ali"))
// Array boyutu
.andExpect(jsonPath("$.length()").value(5))
// İç içe nesneler
.andExpect(jsonPath("$.address.city").value("Istanbul"))
// Wildcard
.andExpect(jsonPath("$[*].name").isArray())
// Matcher'lar
.andExpect(jsonPath("$.price").value(greaterThan(100.0)))
.andExpect(jsonPath("$.name").value(containsString("Lap")))
.andExpect(jsonPath("$.tags").isEmpty())
.andExpect(jsonPath("$.deletedAt").doesNotExist())
// content().json() ile tam JSON karşılaştırma
.andExpect(content().json("""
{"id": 1, "name": "Laptop", "price": 999.99}
"""))
// content().json(expected, true) → strict mode (ekstra alanlar hata verir)@MockBean vs @Mock
| Özellik | @Mock | @MockBean |
|---|---|---|
| Framework | Mockito | Spring Boot Test |
| Context | Spring context dışı | Spring context içi bean'i değiştirir |
| Kullanım | @ExtendWith(MockitoExtension.class) | @WebMvcTest, @SpringBootTest |
| Etki | Sadece test sınıfında | Application context'teki bean'i replace eder |
| Performans | Çok hızlı | Context yeniden oluşturabilir |
// @Mock — Spring context olmadan, saf birim test
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock private UserRepository userRepository;
@InjectMocks private UserService userService;
}
// @MockBean — Spring context içinde bean replace
@WebMvcTest(UserController.class)
class UserControllerTest {
@MockBean private UserService userService; // Context'teki bean replace edilir
}Response Yazdırma (Debug)
Test geliştirirken yanıtı görmek isterseniz:
mockMvc.perform(get("/api/users/1"))
.andDo(print()) // Console'a tam HTTP request/response yazar
.andExpect(status().isOk());@WebMvcTest + MockMvc, controller katmanını hızlı ve izole bir şekilde test etmenin en etkili yoludur. JSON request/response doğrulama, hata durumu testleri ve validation kontrolü — tüm web katmanı endişeleri bu yapıyla kapsanır.
AI Asistan
Sorularını yanıtlamaya hazır