Java测试:从单元测试到端到端测试
高质量的测试是可靠软件的基础。在Java世界中,存在一个成熟的测试金字塔,包含三个主要层级:unit(单元测试)、integration(集成测试)和e2e(端到端测试)。本文将逐一解析这些测试类型,提供Java实践示例,并讲解如何在项目中正确构建测试策略。
1. 单元测试:金字塔的基础
单元测试在隔离环境中验证程序的独立组件——通常是一个类或方法。它们执行速度快,不需要外部依赖(数据库、API),并且应该覆盖大部分业务逻辑。
Java单元测试工具
- JUnit 5 — 编写测试的标准框架。
- Mockito — 用于创建模拟对象的库。
- AssertJ — 提供流畅断言以实现可读性检查。
使用JUnit 5和Mockito的单元测试示例
假设我们有一个依赖PaymentGateway的OrderService服务。我们希望在不调用真实支付网关的情况下测试折扣计算逻辑。
// 待测试类
public class OrderService {
private final PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public double calculateDiscount(double amount) {
if (amount > 1000) {
return amount * 0.1; // 10%折扣
}
return 0;
}
}
// 单元测试
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 static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
@Mock
private PaymentGateway paymentGateway;
@InjectMocks
private OrderService orderService;
@Test
public void testCalculateDiscount_AmountAbove1000_Returns10Percent() {
double result = orderService.calculateDiscount(1500);
assertEquals(150.0, result, 0.001);
}
@Test
public void testCalculateDiscount_AmountBelow1000_ReturnsZero() {
double result = orderService.calculateDiscount(500);
assertEquals(0.0, result);
}
}
最佳实践:使用@BeforeEach初始化公共数据,避免直接测试私有方法,遵循"一个测试只验证一个检查点"的原则。
2. 集成测试:验证交互
集成测试验证多个模块如何协同工作——例如服务与数据库、消息队列或外部API的交互。与单元测试不同,这里使用真实或内存中的依赖。
集成测试工具
- Spring Boot Test — 用于测试Spring应用程序。
- Testcontainers — 用于启动Docker容器(数据库、Kafka)。
- H2 Database — 用于快速测试的内存数据库。
使用Spring Boot的集成测试示例
测试与真实数据库连接的REST控制器(测试中使用H2)。
// 控制器
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new Runti