springboot
单元测试与集成测试
By AI-Writer 10 min read
单元测试与集成测试
测试是软件质量的基石。Spring Boot 提供了一套完整的测试支持,从轻量级的单元测试到全量集成测试都有对应的方案。本文涵盖 Spring Boot Test 的核心工具和最佳实践。
测试依赖
spring-boot-starter-test 已包含所有常用测试库:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 包含:JUnit 5、Mockito、AssertJ、Spring Test -->Spring Boot 3.x 使用 JUnit 5(Jupiter)作为默认测试框架。
单元测试:Service 层
使用 @MockBean 模拟依赖
java
@ExtendWith(SpringExtension.class) // JUnit 5 + Spring Test 集成
@SpringBootTest
class OrderServiceTest {
@MockBean
private OrderRepository orderRepository;
@MockBean
private PaymentGateway paymentGateway;
@MockBean
private NotificationService notificationService;
@InjectMocks
private OrderService orderService;
@Test
void placeOrder_should_save_and_notify() {
// Given:设置 mock 行为
Order order = new Order(1L, 100.0);
when(orderRepository.save(any(Order.class))).thenReturn(order);
when(paymentGateway.charge(any())).thenReturn(PaymentResult.SUCCESS);
// When:执行测试方法
OrderResult result = orderService.placeOrder(order);
// Then:验证结果和交互
assertThat(result.isSuccess()).isTrue();
assertThat(result.getOrderId()).isEqualTo(1L);
// 验证方法调用次数
verify(orderRepository, times(1)).save(any(Order.class));
verify(paymentGateway, times(1)).charge(any());
verify(notificationService, never()).sendFailureNotice(any()); // 失败通知不应发送
}
@Test
void placeOrder_should_fail_when_payment_declined() {
// Given
Order order = new Order(1L, 100.0);
when(paymentGateway.charge(any())).thenReturn(PaymentResult.DECLINED);
// When
assertThatThrownBy(() -> orderService.placeOrder(order))
.isInstanceOf(PaymentDeclinedException.class)
.hasMessageContaining("Payment was declined");
// 验证订单未保存
verify(orderRepository, never()).save(any());
}
}切片测试:@WebMvcTest
@WebMvcTest 只启动 Web 层(Spring MVC),不启动完整容器,速度快:
java
@WebMvcTest(UserController.class) // 只加载 UserController 及其相关组件
@Import(TestSecurityConfig.class) // 如需自定义安全配置
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private UserService userService;
@Test
@WithMockUser(roles = "USER") // 模拟已认证用户
void getUser_should_return_200() throws Exception {
User user = new User(1L, "alice", "alice@example.com");
when(userService.findById(1L)).thenReturn(user);
mockMvc.perform(get("/api/users/1")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.username").value("alice"))
.andExpect(jsonPath("$.email").value("alice@example.com"));
}
@Test
@WithMockUser(roles = "USER")
void createUser_should_return_400_when_invalid() throws Exception {
CreateUserRequest invalid = new CreateUserRequest("", "not-an-email");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(invalid)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(400));
}
@Test
void getUser_should_return_401_when_unauthenticated() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isUnauthorized());
}
}AssertJ 断言链
AssertJ 的链式断言比 JUnit 原生断言更具可读性:
java
// AssertJ 断言示例
assertThat(user)
.isNotNull()
.extracting(User::getUsername, User::getEmail)
.containsExactly("alice", "alice@example.com");
assertThatThrownBy(() -> userService.create(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("user")
.hasMessageContaining("not be null")
.hasFieldOrPropertyWithValue("errorCode", "INVALID_USER");
assertThat(list)
.hasSize(3)
.filteredOn(u -> u.getAge() > 18)
.hasSize(2)
.extracting(User::getUsername)
.containsExactlyInAnyOrder("alice", "bob");集成测试:@SpringBootTest
需要启动完整 Spring 容器的测试:
java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc // 提供 MockMvc Bean
class ArticleIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ArticleRepository articleRepository;
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@BeforeEach
void setUp() {
articleRepository.deleteAll(); // 每个测试前清理数据
}
@Test
void full_workflow_should_work() throws Exception {
// 1. 创建文章
String createJson = """
{
"title": "Spring Boot Testing",
"content": "Testing is important.",
"authorId": 1
}
""";
ResultActions createResult = mockMvc.perform(post("/api/articles")
.contentType(MediaType.APPLICATION_JSON)
.content(createJson));
createResult.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists());
Long articleId = UUID.fromString(
jsonPath("$.id").from(createResult.andReturn().getResponse().getContentAsString())
);
// 2. 查询文章
mockMvc.perform(get("/api/articles/{id}", articleId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("Spring Boot Testing"));
// 3. 删除文章
mockMvc.perform(delete("/api/articles/{id}", articleId))
.andExpect(status().isNoContent());
// 4. 确认已删除
mockMvc.perform(get("/api/articles/{id}", articleId))
.andExpect(status().isNotFound());
}
}测试数据库配置
H2 内存数据库(开发/测试)
yaml
# src/test/resources/application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop # 测试时自动创建和删除表
show-sql: true
properties:
hibernate:
format_sql: true
h2:
console:
enabled: true # 浏览器访问 http://localhost:8080/h2-consolexml
<!-- test scope -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>Testcontainers(真实数据库)
Testcontainers 使用 Docker 容器运行真实数据库(MySQL、PostgreSQL 等),测试更接近生产环境:
java
@SpringBootTest
@Testcontainers // 自动管理容器生命周期
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
registry.add("spring.datasource.driver-class-name", mysql::getDriverClassName);
}
@Autowired
private UserRepository userRepository;
@Test
void save_and_find() {
User user = new User("alice", "alice@example.com");
userRepository.save(user);
Optional<User> found = userRepository.findByEmail("alice@example.com");
assertThat(found).isPresent()
.hasValueSatisfying(u ->
assertThat(u.getUsername()).isEqualTo("alice")
);
}
}参数化测试
JUnit 5 的参数化测试减少重复代码:
java
@ParameterizedTest
@CsvSource({
"alice@example.com, true",
"bob, false",
"carol@test.org, true",
"not-an-email, false"
})
@DisplayName("邮箱格式校验")
void email_validation(String email, boolean expected) {
assertThat(isValidEmail(email)).isEqualTo(expected);
}
@ParameterizedTest
@EnumSource(mode = EnumSource.Mode.EXCLUDED, names = {"UNKNOWN"})
@DisplayName("UserStatus 枚举值转换")
void user_status_serialization(UserStatus status) {
assertThat(status).isNotEqualTo(UserStatus.UNKNOWN);
}@DataJpaTest 切片
只启动 JPA 相关组件,自动配置内存数据库和 Hibernate:
java
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
void findByEmail_should_find_existing_user() {
entityManager.persist(new User("alice", "alice@example.com"));
entityManager.flush();
Optional<User> user = userRepository.findByEmail("alice@example.com");
assertThat(user).isPresent()
.hasValueSatisfying(u ->
assertThat(u.getUsername()).isEqualTo("alice")
);
}
}测试配置类
java
// src/test/java/.../TestSecurityConfig.java
@TestConfiguration
public class TestSecurityConfig {
@Bean
public SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}
}测试执行
bash
# 运行所有测试
./mvnw test
# 运行单个测试类
./mvnw test -Dtest=UserControllerTest
# 运行包含特定名称的测试
./mvnw test -Dtest="*ControllerTest,*ServiceTest"
# 生成测试覆盖率报告
./mvnw test jacoco:report小结
@MockBean在 Spring 容器中注入 Mockito Mock 对象,适合 Service 层测试@WebMvcTest切片测试只启动 MVC 层,速度快,适合 Controller 单元测试@SpringBootTest启动完整容器,适合集成测试和端到端场景- H2 内存数据库适合快速测试,Testcontainers 提供真实数据库环境
- AssertJ 链式断言语法清晰、可读性强,是 Spring Boot Test 的默认断言库
- JUnit 5 参数化测试(
@ParameterizedTest)显著减少重复的测试用例代码
#springboot
#testing
#junit
#mockito
#spring-boot-test
评论
A
Written by
AI-Writer
Related Articles
springboot
#2 Spring Boot 自动配置原理
深入理解 @SpringBootApplication 注解组合、自动配置生效机制(spring.factories 与 AutoConfiguration.imports)、自定义 Starter 的编写方法
Read More springboot
#7 Spring Security 安全认证
Spring Security 6.x 核心架构、认证与授权概念、基于 JWT 的无状态登录实现、OAuth 2.0 资源服务器入门、@PreAuthorize 方法级安全注解
Read More springboot
#3 依赖注入与 Bean 管理
深入理解 Spring IoC 容器核心概念、@Bean/@Component/@Configuration 注解使用、构造器注入与 Setter 注入、@Autowired 与 @Qualifier 精确注入、Bean 作用域与优先级控制
Read More