Spring Testing
1. Unit Testing
JUnit 5 và Mockito
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void whenFindByUsername_thenReturnUser() {
// Given
User user = new User();
user.setUsername("testuser");
user.setEmail("test@example.com");
when(userRepository.findByUsername("testuser"))
.thenReturn(Optional.of(user));
// When
User found = userService.findByUsername("testuser");
// Then
assertNotNull(found);
assertEquals("testuser", found.getUsername());
assertEquals("test@example.com", found.getEmail());
verify(userRepository).findByUsername("testuser");
}
@Test
void whenFindByUsername_thenThrowException() {
// Given
when(userRepository.findByUsername("nonexistent"))
.thenReturn(Optional.empty());
// When & Then
assertThrows(UserNotFoundException.class, () -> {
userService.findByUsername("nonexistent");
});
}
}
Service Layer Testing
@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentService paymentService;
@InjectMocks
private OrderService orderService;
@Test
void whenCreateOrder_thenSuccess() {
// Given
OrderRequest request = new OrderRequest();
request.setProductId(1L);
request.setQuantity(2);
Order order = new Order();
order.setId(1L);
order.setStatus(OrderStatus.CREATED);
when(orderRepository.save(any(Order.class)))
.thenReturn(order);
when(paymentService.processPayment(any(PaymentRequest.class)))
.thenReturn(true);
// When
OrderResponse response = orderService.createOrder(request);
// Then
assertNotNull(response);
assertEquals(OrderStatus.CREATED, response.getStatus());
verify(orderRepository).save(any(Order.class));
verify(paymentService).processPayment(any(PaymentRequest.class));
}
@Test
void whenPaymentFails_thenThrowException() {
// Given
OrderRequest request = new OrderRequest();
when(paymentService.processPayment(any(PaymentRequest.class)))
.thenReturn(false);
// When & Then
assertThrows(PaymentFailedException.class, () -> {
orderService.createOrder(request);
});
}
}
2. Integration Testing
@SpringBootTest
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setup() {
userRepository.deleteAll();
}
@Test
void whenCreateUser_thenStatus201() throws Exception {
UserDto userDto = new UserDto();
userDto.setUsername("newuser");
userDto.setEmail("new@example.com");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(userDto)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.username").value("newuser"))
.andExpect(jsonPath("$.email").value("new@example.com"));
assertTrue(userRepository.findByUsername("newuser").isPresent());
}
@Test
void whenGetNonExistentUser_thenStatus404() throws Exception {
mockMvc.perform(get("/api/users/{id}", 999))
.andExpect(status().isNotFound());
}
}
TestContainers
@SpringBootTest
@Testcontainers
class DatabaseIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void registerPgProperties(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 whenSaveUser_thenSuccess() {
User user = new User();
user.setUsername("testuser");
user.setEmail("test@example.com");
User saved = userRepository.save(user);
assertNotNull(saved.getId());
assertEquals("testuser", saved.getUsername());
}
}
3. API Testing
REST Assured
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ApiIntegrationTest {
@LocalServerPort
private int port;
private String baseUrl;
@BeforeEach
void setup() {
baseUrl = "http://localhost:" + port + "/api";
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
@Test
void whenGetUsers_thenSuccess() {
given()
.contentType(ContentType.JSON)
.when()
.get(baseUrl + "/users")
.then()
.statusCode(200)
.body("size()", greaterThan(0))
.body("[0].username", notNullValue());
}
@Test
void whenCreateInvalidUser_thenBadRequest() {
UserDto invalidUser = new UserDto();
// Missing required fields
given()
.contentType(ContentType.JSON)
.body(invalidUser)
.when()
.post(baseUrl + "/users")
.then()
.statusCode(400)
.body("message", containsString("validation failed"));
}
}
4. Security Testing
WebMvcTest với Security
@WebMvcTest(UserController.class)
@Import(SecurityConfig.class)
class UserControllerSecurityTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@MockBean
private JwtTokenProvider tokenProvider;
@Test
@WithMockUser(roles = "ADMIN")
void whenAdminAccessSecuredEndpoint_thenSuccess() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void whenUserAccessAdminEndpoint_thenForbidden() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
@Test
void whenNoAuth_thenUnauthorized() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isUnauthorized());
}
}
5. Best Practices
1. Test Configuration
@TestConfiguration
public class TestConfig {
@Bean
public TestRestTemplate testRestTemplate() {
return new TestRestTemplate();
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}
2. Test Data Factory
public class TestDataFactory {
public static User createTestUser() {
User user = new User();
user.setUsername("testuser");
user.setEmail("test@example.com");
user.setPassword("password123");
user.setRoles(Set.of(Role.ROLE_USER));
return user;
}
public static Order createTestOrder(User user) {
Order order = new Order();
order.setUser(user);
order.setStatus(OrderStatus.CREATED);
order.setTotalAmount(BigDecimal.valueOf(100.00));
return order;
}
}
3. Custom Annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Test
@Transactional
public @interface TransactionalTest {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Test
@WithMockUser
public @interface AuthenticatedTest {
}
4. Test Utilities
public class TestUtils {
public static String asJsonString(Object obj) {
try {
return new ObjectMapper().writeValueAsString(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static <T> T fromJsonString(String json, Class<T> clazz) {
try {
return new ObjectMapper().readValue(json, clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void clearDatabase(JdbcTemplate jdbcTemplate) {
List<String> tableNames = jdbcTemplate.queryForList(
"SELECT table_name FROM information_schema.tables " +
"WHERE table_schema='public'",
String.class
);
tableNames.forEach(tableName ->
jdbcTemplate.execute("TRUNCATE TABLE " + tableName + " CASCADE")
);
}
}
5. Performance Testing
```java @SpringBootTest public class PerformanceTest {
@Autowired
private UserService userService;
@Test
void testBulkOperationPerformance() {
int numberOfUsers = 1000;
List<User> users = IntStream.range(0, numberOfUsers)
.mapToObj(i -> {
User user = new User();
user.setUsername("user" + i);
user.setEmail("user" + i + "@example.com");
return user;
})
.collect(Collectors.toList());
long startTime = System.currentTimeMillis();
userService.saveAll(users);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
assertTrue(duration < 5000, // 5 seconds
"Bulk operation took too long: " + duration + "ms");
}
}