在Java后端开发中,单元测试是保障代码质量的核心手段。Spring Boot作为主流框架,其单元测试体系随着版本迭代不断优化。本文聚焦于JDK 8与Spring Boot 3的组合场景,深入解析单元测试的多种实现方式,对比不同测试策略的异同,并提供可复用的代码模板。
junit-vintage-engine
依赖)。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.mockitogroupId>
<artifactId>mockito-coreartifactId>
<version>5.3.1version>
<scope>testscope>
dependency>
dependencies>
src/
├── main/
│ ├── java/com/example/demo/
│ │ ├── controller/
│ │ ├── service/
│ │ ├── repository/
│ │ └── DemoApplication.java
└── test/
├── java/com/example/demo/
│ ├── controller/
│ ├── service/
│ └── DemoApplicationTests.java
适用场景:工具类、纯算法逻辑、不依赖Spring组件的代码。
示例代码:
public class MathUtilsTest {
@Test
void testAddition() {
int result = MathUtils.add(2, 3);
Assertions.assertEquals(5, result, "2+3应等于5");
}
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, 7, 9})
void testIsOdd(int number) {
Assertions.assertTrue(MathUtils.isOdd(number));
}
}
优势:
适用场景:需要模拟DAO层或第三方服务的业务逻辑。
示例代码:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void getUserNickname_WhenUserExists_ShouldReturnNickname() {
User mockUser = new User("test", "Test User");
when(userRepository.findByName("test")).thenReturn(mockUser);
String result = userService.getUserNickname("test");
Assertions.assertEquals("Test User", result);
verify(userRepository, times(1)).findByName("test");
}
}
关键技术点:
@Mock
创建虚拟依赖@InjectMocks
自动注入模拟对象verify()
验证交互行为适用场景:需要验证Spring上下文加载、组件间协作。
示例代码:
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Test
void createOrder_ShouldReturn201() throws Exception {
OrderDto orderDto = new OrderDto("P123", 2);
when(orderService.createOrder(any(OrderDto.class)))
.thenReturn(new Order("O456", "P123", 2, "CREATED"));
mockMvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"productId\":\"P123\",\"quantity\":2}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value("O456"));
}
}
关键注解:
@SpringBootTest
:加载完整应用上下文@MockBean
:替换上下文中的真实Bean@AutoConfigureMockMvc
:启用HTTP测试支持适用场景:验证Repository层的CRUD操作。
示例代码:
@DataJpaTest
class ProductRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private ProductRepository productRepository;
@Test
void findByName_WhenProductExists_ShouldReturnProduct() {
Product savedProduct = entityManager.persistFlushFind(
new Product("P123", "Laptop", 999.99));
Optional<Product> found = productRepository.findByName("Laptop");
Assertions.assertTrue(found.isPresent());
Assertions.assertEquals("P123", found.get().getId());
}
}
关键特性:
@ParameterizedTest
@MethodSource("provideTestCases")
void testDiscountCalculation(double originalPrice, double discountRate, double expected) {
Assertions.assertEquals(expected,
PriceCalculator.calculateDiscount(originalPrice, discountRate), 0.001);
}
private static Stream<Arguments> provideTestCases() {
return Stream.of(
Arguments.of(100.0, 0.1, 90.0),
Arguments.of(200.0, 0.25, 150.0),
Arguments.of(500.0, 0.5, 250.0)
);
}
@Test
void testAsyncProcessing() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<String> result = new AtomicReference<>();
asyncService.processData("test", (res) -> {
result.set(res);
latch.countDown();
});
if (!latch.await(5, TimeUnit.SECONDS)) {
Assertions.fail("Timeout waiting for async result");
}
Assertions.assertEquals("PROCESSED:test", result.get());
}
@SpringBootTest
@ActiveProfiles("test")
@TestPropertySource(properties = {
"app.feature.flag=true",
"app.timeout.ms=1000"
})
class FeatureFlagTest {
@Autowired
private FeatureService featureService;
@Test
void testFeatureEnabled() {
Assertions.assertTrue(featureService.isFeatureEnabled());
}
}
测试方式 | 执行速度 | 资源消耗 | 适用场景 | 典型用例 |
---|---|---|---|---|
纯JUnit测试 | 极快 | 极低 | 工具类、算法验证 | 字符串处理、数学计算 |
Mockito测试 | 快 | 低 | Service层逻辑 | 业务规则验证 |
Spring Boot Test | 中等 | 中等 | 组件协作验证 | Controller-Service集成 |
DataJpaTest | 慢 | 高 | 数据库操作验证 | Repository层CRUD |
决策建议:
@Mock
@Spy
或真实对象@MockBean
AAA模式:
@Test
void testPattern() {
// Arrange
User user = new User("test", "password");
// Act
boolean isValid = authService.validate(user);
// Assert
Assertions.assertTrue(isValid);
}
命名规范:
方法名_前置条件_预期结果
calculateDiscount_WhenRateIsZero_ShouldReturnOriginalPrice
分支覆盖:
@Test
void calculateGrade_WhenScoreIs60_ShouldReturnD() {
// 测试边界条件
}
异常测试:
@Test
void withdraw_WhenAmountExceedsBalance_ShouldThrowException() {
Account account = new Account(100.0);
Assertions.assertThrows(InsufficientBalanceException.class,
() -> account.withdraw(150.0));
}
测试数据污染:
@BeforeEach
void setUp() {
// 使用@BeforeEach替代@BeforeClass
// 确保每个测试前重置状态
}
并发测试问题:
@Test
void testConcurrentAccess() throws InterruptedException {
int threadCount = 100;
CountDownLatch latch = new CountDownLatch(threadCount);
ExecutorService executor = Executors.newFixedThreadPool(20);
for (int i = 0; i < threadCount; i++) {
executor.execute(() -> {
try {
counterService.increment();
} finally {
latch.countDown();
}
});
}
latch.await();
Assertions.assertEquals(threadCount, counterService.getCount());
}
特性 | Spring Boot 2 (JDK 8) | Spring Boot 3 (JDK 8兼容模式) | 差异说明 |
---|---|---|---|
测试框架 | JUnit 4/5混合 | 强制使用JUnit 5 | 移除对JUnit 4的直接支持 |
测试配置 | @SpringBootTest |
相同 | 内部实现优化 |
测试切片 | @WebMvcTest 等 |
相同 | 增强对WebFlux的支持 |
测试性能 | 较慢 | 提升15-20% | 优化测试容器启动 |
依赖管理 | Maven/Gradle | 相同 | 推荐使用Gradle 8+ |
在JDK 8与Spring Boot 3的组合场景下,单元测试已形成完整的解决方案体系: