Tại sao testing quan trọng?
Code không có test giống như xây nhà không có móng — trông ổn cho đến khi có thay đổi. Trong Spring Boot, framework hỗ trợ testing rất mạnh.
Các tầng test
┌─────────────────────────────┐
│ E2E Test (ít nhất) │ Selenium, Playwright
├─────────────────────────────┤
│ Integration Test (vừa) │ @SpringBootTest
├─────────────────────────────┤
│ Unit Test (nhiều nhất) │ JUnit + Mockito
└─────────────────────────────┘
Unit Test với JUnit 5
Test một class độc lập, mock tất cả dependencies:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldReturnUserWhenFound() {
// Given
User user = new User(1L, "Alice", "[email protected]");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
// When
User result = userService.findById(1L);
// Then
assertThat(result.getName()).isEqualTo("Alice");
verify(userRepository).findById(1L);
}
@Test
void shouldThrowWhenUserNotFound() {
when(userRepository.findById(99L)).thenReturn(Optional.empty());
assertThrows(UserNotFoundException.class,
() -> userService.findById(99L));
}
}
💡 Given-When-Then
Cấu trúc test rõ ràng: Given (setup), When (action), Then (verify). Giúp test dễ đọc và maintain.
Integration Test với @SpringBootTest
Test toàn bộ Spring context:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setup() {
userRepository.deleteAll();
}
@Test
void shouldCreateUser() {
// Given
CreateUserRequest request = new CreateUserRequest("Alice", "[email protected]");
// When
ResponseEntity<User> response = restTemplate.postForEntity(
"/api/users", request, User.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getName()).isEqualTo("Alice");
assertThat(userRepository.count()).isEqualTo(1);
}
}
Testing API với MockMvc
Test controller layer mà không cần start full server:
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturnUser() throws Exception {
User user = new User(1L, "Alice", "[email protected]");
when(userService.findById(1L)).thenReturn(user);
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Alice"))
.andExpect(jsonPath("$.email").value("[email protected]"));
}
@Test
void shouldReturn404WhenNotFound() throws Exception {
when(userService.findById(99L))
.thenThrow(new UserNotFoundException(99L));
mockMvc.perform(get("/api/users/99"))
.andExpect(status().isNotFound());
}
}
Test Database với @DataJpaTest
Test chỉ JPA layer, nhẹ hơn @SpringBootTest:
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void shouldFindByEmail() {
userRepository.save(new User(null, "Alice", "[email protected]"));
Optional<User> found = userRepository.findByEmail("[email protected]");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Alice");
}
}
Best Practices
- Test behavior, không test implementation — test "kết quả" chứ không test "cách làm"
- Mỗi test độc lập — không phụ thuộc thứ tự chạy
- Tên test mô tả rõ ý —
shouldThrowWhenEmailInvalid>test1 - Unit test nhanh, integration test kỹ — unit test chạy ms, integration test chạy giây
- Test cả happy path lẫn edge case — đừng chỉ test trường hợp thành công