Java 测试:单元测试、集成测试、端到端测试 — 高级水平
在现代 Java 开发中,测试不仅仅是一种形式,而是一项基础实践,它确保了代码的稳定性和可预测性。诸如简单的 @Test 配合 Assert.assertEquals 这类基础测试,早已被复杂的多层策略所取代。在本文中,我们将深入探讨测试金字塔每一层的高级技术:单元测试、集成测试和端到端测试。您将学习如何编写可维护、快速且可靠的测试,让您的代码成为一个运转良好的精密机制。
1. 高级单元测试:模拟、桩和参数化
单元测试用于验证代码中一个独立的片段(通常是一个类或方法)。在高级层面,我们从简单场景转向复杂的交互和边界情况。这里的关键工具是 Mockito 库。
1.1. 使用 ArgumentCaptor 和复杂响应 (Answer)
通常,我们不仅需要验证方法是否被调用,还需要验证传递了哪些参数。这时可以使用 ArgumentCaptor。
// 示例:验证发送到服务的消息是否正确@ExtendWith(MockitoExtension.class)public class NotificationServiceTest { @Mock private EmailSender emailSender; @InjectMocks private NotificationService notificationService;
@Test void shouldSendEmailWithCorrectContent() { // 准备 User user = new User("test@example.com", "Test User"); // 执行 notificationService.sendWelcomeEmail(user); // 断言 ArgumentCaptor<EmailMessage> captor = ArgumentCaptor.forClass(EmailMessage.class); verify(emailSender).send(captor.capture()); EmailMessage capturedMessage = captor.getValue(); assertEquals("test@example.com", capturedMessage.getTo()); assertTrue(capturedMessage.getBody().contains("Test User")); }}要基于输入数据动态创建响应,请使用 Answer 接口:
@Testvoid testDynamicResponse() { when(dataRepository.findById(anyLong())).thenAnswer(invocation -> { Long id = invocation.getArgument(0); return new DataEntity(id, "Entity with ID: " + id); }); assertEquals("Entity with ID: 5", dataService.getDataById(5L).getName());}1.2. 参数化测试 (JUnit 5)
参数化允许使用不同的数据集运行同一个测试,从而避免代码重复。JUnit 5 提供了强大的注解,例如 @CsvSource 和 @MethodSource。
@ParameterizedTest@CsvSource({ "10, 20, 30", "0, 0, 0", "-5, 5, 0"})void testSum(int a, int b, int expectedSum) { Calculator calculator = new Calculator(); assertEquals(expectedSum, calculator.add(a, b));}2. 集成测试:与数据库和外部服务协作
集成测试用于验证系统组件之间的交互,例如与真实数据库或消息队列的交互。主要挑战在于管理环境状态。这时 Testcontainers 就派上了用场。
2.1. 使用 Testcontainers 进行隔离的数据库测试
Testcontainers 会在测试期间启动包含所需软件(PostgreSQL、Redis、Kafka)的 Docker 容器,从而保证环境的清洁和可重现性。
@SpringBootTest@Testcontainersclass UserRepositoryIntegrationTest {
@Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15") .withDatabaseName("testdb") .withUsername("test") .withPassword("test");
@DynamicPropertySource static void configureProperties(DynamicPropertyR