什么是单元测试,什么是集成测试
单元测试是只测试一个特定单元的测试,如果测试需要启动多个层而不是只启动这个被测试单元,那它就是一个集成测试。
测试金字塔
测试金字塔描述了理想测试套件的结构:大量快速的单元测试构成底层,适量的集成测试构成中层,少量的端到端测试构成顶层。单元测试运行速度快、维护成本低,应占测试套件的70%以上;集成测试验证组件协作,约占20%;端到端测试验证完整用户场景,约占10%。遵循金字塔原则能保证测试反馈速度和成本效益的平衡。
测试策略选择
选择测试类型时需权衡速度、可靠性和成本。单元测试适合验证复杂业务逻辑和边界条件,应优先编写;集成测试适合验证组件协作和配置正确性,用于覆盖单元测试无法触及的边界;端到端测试适合验证关键用户路径,但维护成本高,应谨慎使用。
代码分类测试策略
一种比较前沿的观点认为:访问代码、管理者代码、存储代码和业务代码里,只有第四种需要测试,其他的逻辑的正确性只要由顺序执行保证就行了。这第四种测试,是不需要mock的,尽量使用main就能启动。这就要求把业务逻辑和输入输出解耦。和输入解耦比较简单,和输出解耦需要一定的巧思——把业务逻辑写成纯函数也许能达到这一目的。
而集成测试意味着要启动尽可能大的完整程序,进行:
mock object
历史上的 mock object 是为了 peel out 整个 framework,让测试变轻而设计出来的。但如果 mock 的配置比较繁琐,则 mock 仍然很重。
当一个对象的依赖的行为很难定制而需要定制的时候,mock 对象就登场了。在历史上,软件工程的语言里,关于什么是 mock、stub、fake 和 stub 有漫长的争论,它们的共同点是都是真接口的假实现 。
Mockito 已经完成了一个 mock 的精确定义,stub 和 spy 都是 mock 的一些子行为,或者特定类型。
Mockito 核心用法
Stubbing:配置行为
Stubbing 通过 when().thenReturn() 语法配置 mock 对象的行为,这是最常见的 mock 操作。
1 2 3 4 5 6 7 8 import static org.mockito.Mockito.*; List<String> mockedList = mock(List.class);when (mockedList.get(0 )).thenReturn("first" );when (mockedList.get(anyInt())).thenReturn("element" ); assertEquals("first" , mockedList.get(0 )); assertEquals("element" , mockedList.get(999 ));
Verification:验证调用
Mockito 支持验证 mock 对象的方法是否被调用、调用次数、调用顺序等行为。
1 2 3 4 5 6 7 8 9 10 11 List mockedList = mock(List.class); mockedList.add("once" ); mockedList.add("twice" ); mockedList.add("twice" ); verify(mockedList).add("once" ); verify(mockedList, times(2 )).add("twice" ); verify(mockedList, never()).add("never happened" ); verify(mockedList, atLeastOnce()).add("once" ); verify(mockedList, atLeast(2 )).add("twice" ); verify(mockedList, atMost(5 )).add("twice" );
ArgumentCaptor:捕获参数
ArgumentCaptor 用于捕获方法调用时的参数,便于对参数进行断言。
1 2 3 4 5 import org.mockito.ArgumentCaptor; ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); verify(mockedList).add(captor.capture()); assertEquals("actual value" , captor.getValue());
注解驱动 Mock
@Mock 和 @InjectMocks 注解简化了 mock 对象的创建和依赖注入,需要配合 MockitoAnnotations.openMocks() 或 @ExtendWith(MockitoExtension.class) 使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import org.mockito.Mock;import org.mockito.InjectMocks;import org.junit.jupiter.api.extension.ExtendWith;import org.mockito.junit.jupiter.MockitoExtension;@ExtendWith(MockitoExtension.class) class OrderServiceTest { @Mock private InventoryService inventoryService; @Mock private PaymentService paymentService; @InjectMocks private OrderService orderService; @Test void shouldPlaceOrderSuccessfully () { when (inventoryService.checkStock(anyString(), anyInt())).thenReturn(true ); when (paymentService.charge(anyString(), anyBigDecimal())).thenReturn(true ); boolean result = orderService.placeOrder("product-123" , 2 , "user-456" ); assertTrue(result); verify(inventoryService).checkStock("product-123" , 2 ); verify(paymentService).charge("user-456" , new BigDecimal ("199.99" )); } }
Mock vs Stub vs Spy
Mock:验证行为
Mock 对象专注于验证行为,即验证某个方法是否被调用、调用了多少次、调用顺序等。Mockito 的默认 mock 对象就是 Mock。
1 2 3 List mockList = mock(List.class); mockList.add("one" ); verify(mockList).add("one" );
Stub:配置返回值
Stub 是 mock 的一种子行为,专注于配置 mock 对象的返回值,不关心方法是否被调用。
1 2 3 List stubList = mock(List.class);when (stubList.get(0 )).thenReturn("stubbed value" ); assertEquals("stubbed value" , stubList.get(0 ));
Spy:部分 Mock
Spy 是对真实对象的部分 mock,可以保留真实对象的部分行为,同时覆盖其他行为。
1 2 3 4 5 6 7 8 List<String> list = new ArrayList <>(); List<String> spy = spy(list);when (spy.size()).thenReturn(100 ); spy.add("one" ); assertEquals(100 , spy.size()); assertEquals(1 , list.size());
选择建议
使用 Mock 当需要验证方法调用行为时
使用 Stub 当只需要配置返回值时
使用 Spy 当需要保留部分真实行为时
避免过度使用 Spy,通常意味着设计需要重构
🔑 模式提炼:隔离依赖
模式公式 :MOCK {依赖接口} WITH {行为配置} TO ISOLATE {被测单元}
迁移表 :
场景
Mock 对象
行为配置
隔离目标
数据库访问
Repository
when().thenReturn()
业务逻辑
外部 API
HttpClient
stub() 返回固定响应
服务层
缓存操作
CacheManager
verify() 验证缓存命中
缓存策略
消息队列
MessageProducer
ArgumentCaptor 捕获消息
消息发送逻辑
核心洞察 :当被测单元的依赖行为难以控制或运行成本高昂时,通过 mock 对象模拟依赖的行为,实现被测单元的隔离测试。听到"数据库慢"、“外部服务不稳定”、"需要验证调用"时,应想到隔离依赖模式。
Case 分类
参考:
What is the explicit difference between an edge case and a corner case?
What are the difference between an edge case, a corner case, a base case and a boundary case?
edge case
An edge case is a problem or situation that occurs only at an extreme (maximum or minimum ) operating parameter. For example, a stereo speaker might noticeably distort audio when played at its maximum rated volume, even in the absence of other extreme settings or conditions.
边缘情况是仅在极端(最大或最小)操作参数下发生的问题或情况。例如,立体声扬声器在以最大额定音量播放时可能会明显失真,即使没有其他极端设置或条件。
corner case
Corner case occurs outside of normal operating parameters, specifically when multiple environmental variables or conditions are simultaneously at extreme levels, even though each parameter is within the specified range for that parameter. (The “outside normal operating parameters” obviously means something like “outside typical combination of operating parameters”, not strictly “outside allowed operating parameters”. That is, you’re still within the valid parameter space, but near its corner.)
在工程中,极端情况 corner case(或病理情况 pathological case)涉及仅在正常操作参数之外 发生的问题或情况——特别是当多个环境变量或条件同时处于极端水平时表现出来的问题或情况,即使每个参数都在该参数的指定的范围内。
boundary case
Boundary case occurs when one of inputs is at or just beyond maximum or minimum limits.
当输入之一处于或刚好超过最大或最小限制时,就会发生边界情况。
我们常说的 corner case,很大一部分是 boundary case。
base case
Base case is where Recursion ends.
基本情况是递归结束的地方。
developer-side test framework 搭建
框架选择建议
测试框架
JUnit 5 :事实标准,提供强大的断言、参数化测试、嵌套测试等特性
TestNG :功能丰富的替代方案,适合需要数据驱动测试的场景
Spock :基于 Groovy 的 BDD 风格测试框架,适合复杂业务逻辑验证
Mock 框架
Mockito :Java 生态最流行的 mock 框架,API 简洁直观
EasyMock :早期流行的 mock 框架,API 相对繁琐
PowerMock :支持 mock 静态方法和私有方法,但会破坏封装,慎用
断言库
AssertJ :流式 API,提供丰富的断言方法,推荐使用
Hamcrest :匹配器风格,可读性强但链式调用略显冗长
Truth :Google 出品的断言库,API 设计优雅
集成测试搭建步骤
1. 选择测试框架和测试插件
根据项目技术栈选择合适的测试框架组合。Spring Boot 项目推荐使用 JUnit 5 + Mockito + AssertJ。
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency >
2. 确定分包
对于 Gradle 项目,建议使用 source sets 进行测试代码分离:
1 2 3 4 5 6 7 sourceSets { test { java { srcDirs 'src/test/unit' , 'src/test/integration' } } }
3. 建立基类
建立统一的测试基类,封装公共的初始化逻辑和配置。
1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootTest @ActiveProfiles("test") public abstract class BaseIntegrationTest { @Autowired protected TestRestTemplate restTemplate; @BeforeEach void setUp () { } }
4. 确定可插拔的 Mock 方法
全局 Mock 配置
通过配置文件或测试配置类定义全局 mock 行为。
1 2 3 4 5 6 7 8 9 @TestConfiguration public class TestConfig { @Bean @Primary public ExternalService externalService () { return mock(ExternalService.class); } }
局部 Mock 替代
在测试类中通过 @MockBean 替换 Spring 容器中的 bean。
1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootTest class OrderServiceTest { @MockBean private PaymentService paymentService; @Test void shouldPlaceOrder () { when (paymentService.charge(any())).thenReturn(true ); } }
5. 数据准备策略
确定测试数据的准备方式:使用内存数据库(H2)、TestContainers 或固定测试数据集。
6. Case 命名规范
采用统一的测试命名规范,推荐使用 should{ExpectedBehavior}When{StateUnderTest} 格式。
JUnit
JUnit 是事实上的单元测试框架标准,但事实上 JUnit is more than a unit test frame work, but a developer-side test framework。
JUnit 5 is modularized,composed of several modules: platform、vintage 和 jupiter。其中 The JUnit Platform serves as a foundation for launching testing frameworks on the JVM。在上面可以开发针对 JUnit 5 的 testframework,因为提供了专门的 TestEngine API。jupiter 是被重写的新 api,拥有新的 TestEngine 实现,而 vintage 兼容老版本的测试,拥有一个独特的 TestEngine 实现。
当代的 JUnit 已经变成了一个 annotation-driven 运行的 framework 了。全部的生命周期注解可以参考 JUnit 5 用户指南 。
生命周期注解
JUnit 5 提供了完整的生命周期注解,用于控制测试的初始化和清理过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import org.junit.jupiter.api.*;@TestInstance(TestInstance.Lifecycle.PER_CLASS) class LifecycleTest { @BeforeAll static void beforeAll () { System.out.println("BeforeAll - 初始化静态资源" ); } @BeforeEach void setUp () { System.out.println("BeforeEach - 初始化测试数据" ); } @Test void testOne () { System.out.println("Test One" ); } @Test void testTwo () { System.out.println("Test Two" ); } @AfterEach void tearDown () { System.out.println("AfterEach - 清理测试数据" ); } @AfterAll static void afterAll () { System.out.println("AfterAll - 释放静态资源" ); } }
生命周期注解对比
注解
执行时机
方法类型
典型用途
@BeforeAll
所有测试前
static
初始化数据库连接、启动服务器
@BeforeEach
每个测试前
实例
创建测试对象、准备测试数据
@AfterEach
每个测试后
实例
清理测试数据、重置状态
@AfterAll
所有测试后
static
关闭数据库连接、停止服务器
参数化测试
JUnit 5 的 @ParameterizedTest 允许使用不同的输入值多次运行同一个测试方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import org.junit.jupiter.params.ParameterizedTest;import org.junit.jupiter.params.provider.*;@ParameterizedTest @ValueSource(strings = {"racecar", "radar", "level"}) void palindromes (String candidate) { assertTrue(StringUtils.isPalindrome(candidate)); }@ParameterizedTest @CsvSource({"apple, 10", "banana, 20", "orange, 15"}) void testWithCsvSource (String fruit, int quantity) { assertNotNull(fruit); assertTrue(quantity > 0 ); }@ParameterizedTest @MethodSource("provideStringsForIsBlank") void testWithMethodSource (String input, boolean expected) { assertEquals(expected, StringUtils.isBlank(input)); }static Stream<Arguments> provideStringsForIsBlank () { return Stream.of( Arguments.of(null , true ), Arguments.of("" , true ), Arguments.of(" " , true ), Arguments.of("not blank" , false ) ); }
嵌套测试
@Nested 注解用于组织相关的测试方法,使测试结构更清晰,支持嵌套类的生命周期继承。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @DisplayName("Stack 测试") class StackTest { Stack<Object> stack; @BeforeEach void createNewStack () { stack = new Stack <>(); } @Test @DisplayName("新栈应该为空") void isNewStack () { assertTrue(stack.isEmpty()); } @Nested @DisplayName("推入元素后") class AfterPushing { String element = "an element" ; @BeforeEach void pushElement () { stack.push(element); } @Test @DisplayName("栈不应为空") void isNotEmpty () { assertFalse(stack.isEmpty()); } @Test @DisplayName("栈应该返回元素") void returnElement () { assertEquals(element, stack.pop()); } } }
条件执行
JUnit 5 提供了多种条件执行注解,根据运行环境或配置决定是否执行测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Test @EnabledOnOs(OS.MAC) void onlyOnMac () { }@Test @DisabledOnJre(JRE.JAVA_8) void notOnJava8 () { }@Test @EnabledIfSystemProperty(named = "test.env", matches = "ci") void onlyInCI () { }@Test @EnabledIf("customCondition()") void customCondition () { }boolean customCondition () { return System.getenv().containsKey("SPECIAL_ENV" ); }
断言 API
JUnit 5 提供了丰富的断言方法,支持分组断言、异常断言、超时断言等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 import static org.junit.jupiter.api.Assertions.*;@Test void groupedAssertions () { assertAll("person" , () -> assertEquals("John" , person.getFirstName()), () -> assertEquals("Doe" , person.getLastName()), () -> assertEquals("john.doe@example.com" , person.getEmail()) ); }@Test void exceptionTesting () { Exception exception = assertThrows(IllegalArgumentException.class, () -> { person.setAge(-1 ); }); assertEquals("Age cannot be negative" , exception.getMessage()); }@Test void timeoutNotExceeded () { assertTimeout(ofMillis(100 ), () -> { Thread.sleep(50 ); }); }@Test void timeoutExceeded () { assertTimeoutPreemptively(ofMillis(100 ), () -> { Thread.sleep(200 ); }); } # Spring 与 Test ## Unit Test ### Mock Object org.springframework.mock 下有 env、http、jndi、web 四个子包。它实际上包含 Environment、JNDI、Servlet API、Spring Web Reactive 这四类 mock objects。所谓的 mock objects 实际上是对一些 Spring 传统 API 接口的 mock implementation。 ### Unit Testing Support Classes #### General Testing Utilities - **AopTestUtils**:用于获取 AOP 代理背后的目标对象 - **ReflectionTestUtils**:通过反射访问和修改私有字段和方法 - **TestSocketUtils**:查找可用的 TCP/UDP 端口 #### Spring MVC Testing Utilities - **ModelAndViewAssert**:用于断言 ModelAndView 的内容 ### MockMvc 独立模式 MockMvc 独立模式无需启动完整的 Spring 容器,仅加载 Web 层组件,适合测试 Controller 层逻辑。 ```javaimport org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.setup.MockMvcBuilders;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;class UserControllerTest { private MockMvc mockMvc; @BeforeEach void setUp () { UserController controller = new UserController (userService); mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); } @Test void shouldReturnUser () throws Exception { mockMvc.perform(get("/api/users/1" )) .andExpect(status().isOk()) .andExpect(jsonPath("$.id" , is(1 ))) .andExpect(jsonPath("$.name" , is("John" ))); } }
@WebMvcTest
@WebMvcTest 是 Spring Boot 提供的测试切片注解,仅加载 Web 层相关组件,自动配置 MockMvc。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @WebMvcTest(UserController.class) class UserControllerWebMvcTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void shouldReturnUser () throws Exception { when (userService.findById(1L )).thenReturn(new User (1L , "John" )); mockMvc.perform(get("/api/users/1" )) .andExpect(status().isOk()) .andExpect(jsonPath("$.name" , is("John" ))); } }
Integration Test
如果需要 deploy a server、connecting to other enterprise infrastructure、init a context。
集成测试的支持包括:
管理 Spring IoC 容器在测试之间的缓存
为测试 fixture 实例提供依赖注入,对固定装置对象的注入对于测试的帮助很大
提供适合集成测试的事务管理
提供帮助开发者编写集成测试的 Spring 特定基类
@SpringBootTest
@SpringBootTest 启动完整的 Spring 应用上下文,用于编写集成测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") class OrderIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private OrderRepository orderRepository; @Test void shouldCreateOrder () throws Exception { String orderJson = "{\"productId\": 1, \"quantity\": 2}" ; mockMvc.perform(post("/api/orders" ) .contentType(MediaType.APPLICATION_JSON) .content(orderJson)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id" ).exists()); } }
TestContext 缓存机制
Spring TestContext Framework 提供 ApplicationContext 缓存机制,避免在多个测试类之间重复启动容器。默认情况下,相同配置的测试类会共享同一个 ApplicationContext。
1 2 3 4 5 6 @SpringBootTest class TestClass1 { }@SpringBootTest class TestClass2 { }
@Transactional 回滚
测试方法上的 @Transactional 注解默认在测试结束后回滚事务,保持数据库干净。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @SpringBootTest @Transactional class OrderRepositoryTest { @Autowired private OrderRepository orderRepository; @Test void shouldSaveOrder () { Order order = new Order ("product-123" , 2 ); orderRepository.save(order); assertNotNull(order.getId()); } }
@Sql 数据准备
@Sql 注解用于在测试执行前或执行后执行 SQL 脚本,准备或清理测试数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SpringBootTest @Sql("/test-data.sql") class OrderServiceTest { @Test void shouldFindAllOrders () { List<Order> orders = orderService.findAll(); assertEquals(3 , orders.size()); } @Test @Sql(scripts = "/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) void shouldCleanUpAfterTest () { } }
JDBC Testing Support
Spring Test 提供 JdbcTestUtils 用于简化数据库测试的 SQL 操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import org.springframework.test.context.jdbc.Sql;import org.springframework.jdbc.core.JdbcTemplate;@SpringBootTest class JdbcTest { @Autowired private JdbcTemplate jdbcTemplate; @Test @Sql(scripts = "/schema.sql", executionPhase = BEFORE_TEST_METHOD) void shouldCountRows () { int count = JdbcTestUtils.countRowsInTable(jdbcTemplate, "users" ); assertEquals(5 , count); JdbcTestUtils.deleteFromTables(jdbcTemplate, "users" ); assertEquals(0 , JdbcTestUtils.countRowsInTable(jdbcTemplate, "users" )); } }
Spring TestContext Framework
Spring TestContext Framework 提供了测试执行的核心基础设施,支持:
ApplicationContext 的加载和管理
TestExecutionListener 的扩展机制
测试上下文的缓存和共享
WebTestClient
WebTestClient 是用于测试 WebFlux 应用的响应式测试客户端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @SpringBootTest @AutoConfigureWebTestClient class UserControllerTest { @Autowired private WebTestClient webTestClient; @Test void shouldReturnUser () { webTestClient.get().uri("/api/users/1" ) .exchange() .expectStatus().isOk() .expectBody(User.class) .isEqualTo(new User (1L , "John" )); } }
Testing Client Applications
使用 TestRestTemplate 测试 REST 客户端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SpringBootTest(webEnvironment = RANDOM_PORT) class ClientIntegrationTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Test void shouldCallExternalApi () { String url = "http://localhost:" + port + "/api/users/1" ; ResponseEntity<User> response = restTemplate.getForEntity(url, User.class); assertEquals(HttpStatus.OK, response.getStatusCode()); } }
测试切片注解
Spring Boot 提供了多个测试切片注解,用于测试特定层而不加载完整的应用上下文。
@DataJpaTest
@DataJpaTest 仅加载 JPA 相关组件,配置内存数据库,适合测试 Repository 层。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @DataJpaTest class UserRepositoryTest { @Autowired private TestEntityManager entityManager; @Autowired private UserRepository userRepository; @Test void shouldFindUserByEmail () { User user = new User ("john@example.com" , "John" ); entityManager.persist(user); Optional<User> found = userRepository.findByEmail("john@example.com" ); assertTrue(found.isPresent()); assertEquals("John" , found.get().getName()); } }
@WebMvcTest
@WebMvcTest 仅加载 Web 层组件,自动配置 MockMvc。
@JsonTest
@JsonTest 仅加载 JSON 序列化相关组件,测试 JSON 序列化和反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @JsonTest class UserJsonTest { @Autowired private JacksonTester<User> json; @Test void shouldSerializeUser () throws JsonProcessingException { User user = new User (1L , "John" ); assertThat(json.write(user)).extractingJsonPathStringValue("$.name" ) .isEqualTo("John" ); } @Test void shouldDeserializeUser () throws IOException { String content = "{\"id\":1,\"name\":\"John\"}" ; User user = json.parse(content).getObject(); assertEquals("John" , user.getName()); } }
Annotations
Spring Test 提供的核心注解:
@SpringBootTest:启动完整应用上下文
@WebMvcTest:测试 Web 层
@DataJpaTest:测试 JPA 层
@MockBean:替换 Spring 容器中的 bean
@TestConfiguration:提供测试特定的配置
@ActiveProfiles:激活指定的 profile
@Transactional:测试方法事务管理
@Sql:执行 SQL 脚本
Annotations
测试最佳实践
Given-When-Then 模式
Given-When-Then 是一种测试结构模式,将测试分为三个清晰的阶段:准备数据、执行操作、验证结果。
1 2 3 4 5 6 7 8 9 10 11 12 @Test void shouldCalculateTotalPrice () { Product product = new Product ("Laptop" , 1000.0 ); int quantity = 2 ; BigDecimal total = orderService.calculateTotal(product, quantity); assertEquals(new BigDecimal ("2000.00" ), total); }
这种结构使测试意图清晰,便于阅读和维护。
测试命名规范
良好的测试命名应该清晰表达测试的目的和预期行为。推荐使用 should{ExpectedBehavior}When{StateUnderTest} 格式。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test void shouldReturnOrderNotFoundWhenOrderIdDoesNotExist () { }@Test void shouldThrowExceptionWhenQuantityIsNegative () { }@Test void testOrder () { }@Test void test1 () { }
测试覆盖率策略
测试覆盖率是衡量测试完整性的指标,但不是唯一标准。合理的覆盖率策略应考虑:
行覆盖率 :至少达到 80%,核心业务代码应达到 100%
分支覆盖率 :确保所有条件分支都被测试
方法覆盖率 :公共 API 应该有完整的测试覆盖
避免为了追求覆盖率而编写无意义的测试,测试应该关注业务逻辑的正确性和边界条件。
🔑 模式提炼:测试三段式
模式公式 :GIVEN {测试准备} WHEN {执行操作} THEN {验证结果}
迁移表 :
测试类型
Given
When
Then
单元测试
准备 mock 对象和输入数据
调用被测方法
断言返回值和验证 mock
集成测试
准备数据库数据和请求
发送 HTTP 请求
断言响应状态和内容
端到端测试
登录用户并进入页面
执行用户操作
验证页面元素和状态
核心洞察 :所有测试都可以抽象为准备、执行、验证三个阶段。听到"测试结构不清晰"、"测试难以理解"时,应想到测试三段式模式。
测试反模式
过度 Mock
过度 mock 是指对不应该 mock 的依赖进行 mock,导致测试失去了验证真实行为的能力。
1 2 3 4 5 6 7 8 9 10 11 @Test void shouldCreateOrder () { Order order = new Order (); when (orderRepository.save(order)).thenReturn(order); when (orderService.calculateTotal(any())).thenReturn(BigDecimal.ZERO); orderService.createOrder(order); verify(orderRepository).save(order); }
过度 mock 的后果包括:测试与实现细节耦合、无法发现设计问题、重构困难。
避免方法 :只在测试真正的外部依赖时使用 mock,如数据库、外部 API、文件系统等。
脆弱测试
脆弱测试是指对实现细节过于敏感的测试,代码重构时容易失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test void shouldProcessOrder () { Order order = new Order (); orderService.processOrder(order); assertEquals("PROCESSING" , order.getStatus()); assertEquals(1 , order.getAuditLog().size()); }@Test void shouldSendOrderConfirmationEmail () { Order order = new Order (); orderService.processOrder(order); verify(emailService).sendConfirmationEmail(order.getCustomerEmail()); }
避免方法 :测试公共 API 和可观察的行为,而非内部实现细节。
测试间依赖
测试间依赖是指测试的执行依赖于其他测试的状态,导致测试无法独立运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @SpringBootTest class OrderRepositoryTest { @Autowired private OrderRepository orderRepository; @Test void shouldCreateOrder () { Order order = new Order ("product-123" , 1 ); orderRepository.save(order); } @Test void shouldFindAllOrders () { List<Order> orders = orderRepository.findAll(); assertFalse(orders.isEmpty()); } }@SpringBootTest @Transactional class OrderRepositoryTest { @Autowired private OrderRepository orderRepository; @Test void shouldCreateOrder () { Order order = new Order ("product-123" , 1 ); orderRepository.save(order); assertNotNull(order.getId()); } @Test void shouldFindAllOrders () { Order order = new Order ("product-456" , 2 ); orderRepository.save(order); List<Order> orders = orderRepository.findAll(); assertEquals(1 , orders.size()); } }
避免方法 :每个测试应该是独立的,使用 @Transactional 回滚或 @BeforeEach/@AfterEach 清理数据。
🔑 模式提炼:测试独立性
模式公式 :ENSURE {测试隔离} BY {独立准备} AND {独立清理} AND {无共享状态}
迁移表 :
场景
隔离方式
清理方式
避免共享
数据库测试
@Transactional 回滚
自动回滚
每个测试独立准备数据
文件系统测试
临时目录
@AfterEach 删除
使用唯一文件名
内存状态测试
每次新建对象
GC 自动回收
避免静态变量
Mock 对象
@MockBean 每次重新创建
自动重置
不跨测试共享
核心洞察 :测试必须独立运行,顺序无关。听到"测试有时通过有时失败"、"单独运行通过但批量运行失败"时,应想到测试独立性模式。