75 phút
Testing trong Spring Boot
Test Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
Unit Tests
Service test với Mockito
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private UserService userService;
@Test
void createUser_shouldReturnCreatedUser() {
// Given
var request = new CreateUserRequest("John", "john@example.com", "password");
var user = User.builder()
.id(1L)
.name("John")
.email("john@example.com")
.build();
when(userRepository.existsByEmail(anyString())).thenReturn(false);
when(passwordEncoder.encode(anyString())).thenReturn("encoded");
when(userRepository.save(any(User.class))).thenReturn(user);
// When
UserResponse response = userService.create(request);
// Then
assertThat(response.id()).isEqualTo(1L);
assertThat(response.name()).isEqualTo("John");
verify(userRepository).save(any(User.class));
}
@Test
void createUser_withExistingEmail_shouldThrow() {
when(userRepository.existsByEmail(anyString())).thenReturn(true);
assertThatThrownBy(() -> userService.create(
new CreateUserRequest("John", "john@example.com", "password")
))
.isInstanceOf(ConflictException.class)
.hasMessageContaining("Email already exists");
verify(userRepository, never()).save(any());
}
}
Controller Tests với MockMvc
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
void createUser_shouldReturnCreated() throws Exception {
var request = new CreateUserRequest("John", "john@example.com", "password");
var response = new UserResponse(1L, "John", "john@example.com", "USER", LocalDateTime.now());
when(userService.create(any())).thenReturn(response);
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John"))
.andExpect(jsonPath("$.email").value("john@example.com"));
}
@Test
void createUser_withInvalidEmail_shouldReturn400() throws Exception {
var request = new CreateUserRequest("John", "invalid-email", "password");
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.details.email").exists());
}
@Test
void getUser_whenNotExists_shouldReturn404() throws Exception {
when(userService.findById(999L))
.thenThrow(new NotFoundException("User", 999L));
mockMvc.perform(get("/api/v1/users/999"))
.andExpect(status().isNotFound());
}
}
Integration Tests
Full context test với Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@Transactional
class UserIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void props(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 TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@Test
void createAndFetchUser() {
var request = new CreateUserRequest("John", "john@example.com", "password123");
var createResponse = restTemplate.postForEntity(
"/api/v1/users", request, UserResponse.class);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody().name()).isEqualTo("John");
Long id = createResponse.getBody().id();
var getResponse = restTemplate.getForEntity(
"/api/v1/users/" + id, UserResponse.class);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().email()).isEqualTo("john@example.com");
}
}
Test Data Builders
public class UserTestBuilder {
private String name = "Test User";
private String email = "test@example.com";
private UserRole role = UserRole.USER;
public UserTestBuilder withName(String name) {
this.name = name;
return this;
}
public UserTestBuilder withEmail(String email) {
this.email = email;
return this;
}
public User build() {
return User.builder()
.name(name)
.email(email)
.role(role)
.password("encoded")
.build();
}
}
Testing Security
@WebMvcTest(UserController.class)
@Import(SecurityConfig.class)
class SecuredControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@MockBean
private JwtService jwtService;
@Test
@WithMockUser(roles = "ADMIN")
void adminEndpoint_shouldAllowAdmin() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void adminEndpoint_shouldDenyUser() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
@Test
void securedEndpoint_withoutAuth_shouldReturn401() throws Exception {
mockMvc.perform(get("/api/v1/users"))
.andExpect(status().isUnauthorized());
}
}
Test Configuration
@TestConfiguration
public class TestConfig {
@Bean
@Primary
public PasswordEncoder testPasswordEncoder() {
return new BCryptPasswordEncoder(4);
}
}
application-test.yml
spring:
jpa:
hibernate:
ddl-auto: create-drop
show-sql: false
flyway:
enabled: false
logging:
level:
root: WARN
com.example: DEBUG
Bài tập thực hành
Hãy viết tests toàn diện cho User API!