Testing Spring Boot: Hướng Dẫn Toàn Diện với JUnit & Mockito

Giới Thiệu
Testing là yếu tố quan trọng để xây dựng ứng dụng Spring Boot đáng tin cậy và dễ bảo trì. Test coverage toàn diện đảm bảo code hoạt động đúng, phát hiện lỗi sớm và giúp refactoring an toàn hơn.
Trong hướng dẫn toàn diện này, chúng ta sẽ tìm hiểu:
- Các nguyên tắc cơ bản về testing và test pyramid
- Unit testing với JUnit 5 và Mockito
- Integration testing với Spring Boot Test
- Testing REST APIs với MockMvc
- Database testing với TestContainers
- Security testing cho các endpoint được xác thực
- Phương pháp hay nhất cho Test-Driven Development (TDD)
- Code coverage và continuous testing
Yêu cầu: Bài hướng dẫn này xây dựng dựa trên Spring Boot Security. Đảm bảo bạn đã có ứng dụng Spring Boot hoạt động với JPA và Security.
Test Pyramid
Một ứng dụng được test tốt tuân theo test pyramid:
/\
/ \ E2E Tests (Ít)
/____\
/ \
/ Integr \ Integration Tests (Vừa)
/__________\
/ \
/ Unit Tests \ Unit Tests (Nhiều)
/________________\Unit Tests (70%): Tests nhanh, độc lập cho từng component
Integration Tests (20%): Test tương tác giữa các components
End-to-End Tests (10%): Test toàn bộ luồng người dùng
Thiết Lập Dependencies
Cập nhật pom.xml:
<dependencies>
<!-- Spring Boot Test Starter (bao gồm JUnit 5, Mockito, AssertJ) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Security Test -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- TestContainers cho database testing -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<!-- H2 Database cho in-memory testing -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>Unit Testing với JUnit 5
Testing Service Layer
Unit tests nên nhanh và độc lập. Sử dụng Mockito để mock dependencies.
src/test/java/com/example/demo/service/UserServiceTest.java:
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("UserService Unit Tests")
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
private User testUser;
@BeforeEach
void setUp() {
testUser = new User("johndoe", "[email protected]", "password123");
testUser.setId(1L);
}
@Test
@DisplayName("Nên tạo user thành công")
void testCreateUser_Success() {
// Given
when(userRepository.existsByUsername(anyString())).thenReturn(false);
when(userRepository.existsByEmail(anyString())).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(testUser);
// When
User createdUser = userService.createUser(testUser);
// Then
assertThat(createdUser).isNotNull();
assertThat(createdUser.getUsername()).isEqualTo("johndoe");
verify(userRepository, times(1)).save(any(User.class));
}
@Test
@DisplayName("Nên throw exception khi username đã tồn tại")
void testCreateUser_UsernameExists() {
// Given
when(userRepository.existsByUsername("johndoe")).thenReturn(true);
// When & Then
assertThatThrownBy(() -> userService.createUser(testUser))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Username already exists");
verify(userRepository, never()).save(any(User.class));
}
}Các khái niệm chính:
@ExtendWith(MockitoExtension.class): Kích hoạt Mockito@Mock: Tạo mock dependencies@InjectMocks: Inject mocks vào service@BeforeEach: Setup trước mỗi test- AssertJ: Assertions rõ ràng (
assertThat())
Integration Testing
Testing Repository Layer
src/test/java/com/example/demo/repository/UserRepositoryIntegrationTest.java:
package com.example.demo.repository;
import com.example.demo.entity.User;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
@DataJpaTest
@DisplayName("UserRepository Integration Tests")
class UserRepositoryIntegrationTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
@DisplayName("Nên lưu và truy xuất user")
void testSaveAndFindUser() {
// Given
User user = new User("testuser", "[email protected]", "pass");
// When
User savedUser = userRepository.save(user);
entityManager.flush();
// Then
assertThat(savedUser.getId()).isNotNull();
Optional<User> found = userRepository.findById(savedUser.getId());
assertThat(found).isPresent();
assertThat(found.get().getUsername()).isEqualTo("testuser");
}
@Test
@DisplayName("Nên tìm user theo username")
void testFindByUsername() {
// Given
User user = new User("johndoe", "[email protected]", "pass");
entityManager.persist(user);
entityManager.flush();
// When
Optional<User> found = userRepository.findByUsername("johndoe");
// Then
assertThat(found).isPresent();
assertThat(found.get().getEmail()).isEqualTo("[email protected]");
}
}Testing REST Controllers với MockMvc
src/test/java/com/example/demo/controller/UserControllerTest.java:
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Optional;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UserController.class)
@DisplayName("UserController REST API Tests")
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
@DisplayName("GET /api/users/{id} - Nên trả về user khi đã xác thực")
@WithMockUser
void testGetUser_Success() throws Exception {
// Given
User user = new User("johndoe", "[email protected]", "pass");
user.setId(1L);
when(userService.getUserById(1L)).thenReturn(Optional.of(user));
// When & Then
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.username", is("johndoe")));
}
@Test
@DisplayName("GET /api/users/{id} - Nên trả về 401 khi chưa xác thực")
void testGetUser_Unauthorized() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isUnauthorized());
}
@Test
@DisplayName("GET /api/users - Nên trả về 403 cho non-admin")
@WithMockUser(roles = "USER")
void testGetAllUsers_Forbidden() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isForbidden());
}
}Testing với TestContainers
TestContainers cung cấp database thực cho integration tests.
package com.example.demo;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@Testcontainers
public abstract class AbstractIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}Best Practices
1. Theo Mẫu AAA
@Test
void testExample() {
// Arrange: Chuẩn bị dữ liệu test
User user = new User("test", "[email protected]", "pass");
// Act: Thực thi method cần test
User result = userService.createUser(user);
// Assert: Xác minh kết quả
assertThat(result).isNotNull();
}2. Sử Dụng Tên Test Mô Tả
// Tốt
@Test
void shouldReturnUserWhenValidIdProvided() { }
// Không tốt
@Test
void test1() { }3. Test Một Điều Mỗi Test
// Tốt
@Test
void shouldValidateEmail() { }
@Test
void shouldValidatePassword() { }4. Mock External Dependencies
@Mock
private EmailService emailService;
@Test
void shouldSendWelcomeEmail() {
userService.createUser(testUser);
verify(emailService).sendWelcomeEmail(testUser.getEmail());
}Code Coverage
Đo Lường với JaCoCo
Thêm vào pom.xml:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>Chạy tests với coverage:
mvn clean test
mvn jacoco:reportXem báo cáo: target/site/jacoco/index.html
Mục Tiêu Coverage
- Hướng tới 80%+ overall coverage
- Critical paths: 90%+ coverage
- Đừng quá ám ảnh 100%: Tập trung vào tests có ý nghĩa
Kết Luận
Testing toàn diện là yếu tố thiết yếu để xây dựng ứng dụng Spring Boot đáng tin cậy. Các điểm chính:
- Tuân theo test pyramid: Nhiều unit tests, vừa integration tests, ít E2E tests
- Sử dụng JUnit 5 và Mockito: Frameworks testing chuẩn cho Java
- MockMvc cho API testing: Test controllers không cần start server
- TestContainers cho integration: Database instances thực
- Thực hành TDD: Viết tests trước, rồi implementation
- Hướng tới high coverage: Nhưng tập trung vào tests có ý nghĩa
Code được test tốt dễ maintain, refactor và mở rộng hơn.
Tài Liệu Tham Khảo
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.