如何写出好的单元测试?

本文强调了编写良好单元测试的重要性,并提出了五个关键原则:保持测试代码紧凑和可读性,避免重复断言,全面覆盖代码路径,谨慎使用Mock,以及不过度Mock。遵循这些原则有助于确保测试的有效性和生产代码的健康状态。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

大家都知道,开发软件的时候为代码编写单元测试是很好的。但实际上,光有测试还不够,还要编写好的测试,这同样重要。

要做到这一点,考虑遵循一些固执的原则,对测试代码给予一些关爱:

1. 保持测试代码的紧凑和可读性

要做到这一点,应该要进行毫不留情的重构,就像对生产代码应该做的那样。否则让测试代码随着时间腐化,就是在测试里面制造可怕的遗留代码。如果测试不能很容易重构,那么生产代码也很难重构,从而导致生产系统的遗留代码。始终做一个勇敢的重构者。

2. 避免编写重复累赘的断言

举个例子,测试代码使用正则表达式生成内容,而这个正则表达式是跟生产代码的解析器中使用的一模一样的。

一般来说,我们不希望在测试和代码之间复制逻辑。因此,在测试中复制正则表达式或其他内容不是一种选择。在这种情况下,考虑测试输入激励/输出结果之间的关系(f(输入) - >输出)可能会有帮助,例如,如果代码的目标是要做模板替换,不要在测试代码里用原始值来做替换。相反,在测试里面直接指定预期的计算结果。

// 使用
Assertions.assertThat(processTemplate("param1", "param2")).isEqualTo("this is 'param1', and this is 'param2'"));

// 而不要用
Assertions.assertThat(processTemplate("param1", "param2")).isEqualTo("this is '%s', and this is '%s'", param1, param2));
复制代码

3. 覆盖尽可能多的范围,包括正面情况,以及(甚至更重要的)出错的代码路径。

通常,要做到这一点,最好的办法试采用测试驱动开发(Test Driven Development)。通过TDD,人们可以在设计时识别可能会出错的部分。不要羞于为一段小代码编写一个简单的测试用例。你永远不知道什么时候,为什么以及以什么方式,你会要用到甚至修改这段代码。

如果对软件测试、接口测试、自动化测试、性能测试、LR脚本开发、面试经验交流。感兴趣可以273462828,群内会有不定期的发放免费的资料链接,这些资料都是从各个技术网站搜集、整理出来的,如果你有好的学习资料可以私聊发我,我会注明出处之后分享给大家。

可以研究一下如何检查测试的有效性,类似PIT这样的工具可以进行变更测试,值得研究一下。

4. 不要Mock你不拥有的类型!

这不是一个硬界限,但越过这条线很可能会产生反作用力!

TDD是关于设计的,也是关于测试的,两者一样重要,在模拟外部API时,测试不能用于驱动设计,API属于第三方;这个第三方可以,并且实际上也经常会更改API的签名和行为。

想象一下Mock第三方Lib的代码。在第三方库的某次升级之后,它的逻辑可能会改变,但测试套件仍会执行得很好,因为它被Mock了。所以后来,你认为一切都很好,毕竟构建墙是绿色的,软件部署上去,然后......嘣

如果你感觉需要Mock第三方库,可能表明你当前的设计与第三方库没有足够的分离。

另一个问题是第三方库可能很复杂,需要大量的Mock才能正常工作。这导致过度指定的测试和复杂的测试辅助装置,这本身就损害了紧凑和可读的目标。或者由于模拟外部系统过于复杂,从而导致测试代码对生产代码的覆盖不足。

取而代之的最常见的方法,是围绕外部lib / 系统创建包装器,尽管应该意识到抽象泄漏的风险,其中过多的低级API,概念或异常超出了包装器的边界。为了验证与第三方库的集成,编写集成测试,并使它们尽可能紧凑和可读。

5. 不要Mock一切,这是一种反模式

如果一切都被Mock,我们真的在测试生产代码吗?该不Mock的时候,不要犹豫!

不要Mock值对象

为什么人们甚至想要这样做?

因为实例化对象太痛苦了! => 不是正当理由。

如果创建新的对象太难了,那么代码可能需要一些严肃的重构。另一种方法是为您的值对象创建构建器 - 有一些工具,包括IDE插件,Lombok和其他。还可以在测试类路径中创建有意义的工厂方法。

abstract class CustomerCreations {
   public static Customer customer_with_a_single_item_in_the_basket() {
       // long init sequence
   }
}
复制代码
<think>好的,用户想知道如何在Spring Boot中编单元测试,包括示例教程和最佳实践。首先,我需要根据用户提供的引用资料来整理回答的结构。用户提到了三个引用,分别涉及JUnit 5和Mockito的集成、Web层测试的不同方法,以及使用@WebMvcTest进行控制器的测试。 首先,我应该分步骤来讲解,可能包括环境准备、不同层次的测试方法(如服务层、Web层)、最佳实践等。每个部分需要结合示例代码,确保用户能够理解具体的实现方式。例如,在服务层测试中,使用Mockito来模拟依赖,这样测试可以专注于被测试的类而不受外部影响。在Web层测试时,可以用@WebMvcTest和MockMvc,或者@SpringBootTest结合TestRestTemplate,这两种方法的区别和适用场景需要说明清楚。 然后,我需要考虑最佳实践部分,比如测试的独立性、模拟外部依赖、分层测试等,这些都是用户可能关心的点。同时,要确保代码示例的格式正确,使用正确的注解和语法,比如@ExtendWith(MockitoExtension.class)或者@MockBean的使用。 另外,用户可能对如何组织测试代码、如何验证结果(如断言的使用)以及如何处理集成测试和单元测试的区别感兴趣。需要区分单元测试和集成测试的不同方法,比如使用@WebMvcTest进行轻量级测试,而@SpringBootTest用于更全面的测试。 还要注意引用资料的标注,比如在讲解到Mockito模拟时引用[^1],在提到TestRestTemplate时引用[^2],在讨论@WebMvcTest时引用[^3]。确保每个引用都自然地出现在相关内容的末尾,不显得突兀。 最后,生成的相关问题需要围绕用户可能进一步想了解的内容,比如如何测试数据库层、性能优化方法、测试覆盖率工具等,这些都是在单元测试中常见的后续问题。 总结下来,回答的结构应该是分步骤讲解,每个步骤包含示例代码和解释,然后列出最佳实践,最后附上相关问题。需要确保语言简洁明了,符合中文表达习惯,同时正确应用LaTeX格式,比如在提到版本时使用$Spring Boot 3.x$,不过根据系统指令,可能不需要特别处理,保持自然即可。</think>### 如何在 Spring Boot 中编单元测试(示例教程与最佳实践) #### 一、单元测试基础配置 1. **依赖准备** 在`pom.xml`中添加测试依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> ``` 该依赖包含 JUnit 5、Mockito 和 AssertJ 等核心测试工具。 2. **测试类结构** 使用`@SpringBootTest`注解启动应用上下文,或按需选择轻量级注解(如`@WebMvcTest`)。 示例: ```java @SpringBootTest class UserServiceTest { @Autowired private UserService userService; } ``` --- #### 二、不同层次的单元测试实现 ##### 1. **服务层测试(Service Layer)** - **使用 Mockito 模拟依赖** 通过`@Mock`创建模拟对象,`@InjectMocks`注入被测试对象: ```java @ExtendWith(MockitoExtension.class) class OrderServiceTest { @Mock private PaymentGateway paymentGateway; @InjectMocks private OrderService orderService; @Test void shouldCreateOrder() { when(paymentGateway.process(any())).thenReturn(true); Order result = orderService.createOrder(new OrderRequest()); assertNotNull(result); } } ``` ##### 2. **Web 层测试(Controller Layer)** - **使用 `@WebMvcTest` 轻量级测试** 仅加载 MVC 相关配置,适合单独测试控制器: ```java @WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void shouldReturnUser() throws Exception { when(userService.findById(1L)).thenReturn(new User("Alice")); mockMvc.perform(get("/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("Alice")); } } ``` 此方式启动速度快且隔离性好。 - **完整 HTTP 环境测试** 使用`@SpringBootTest`启动随机端口,结合`TestRestTemplate`: ```java @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class APIClientTest { @Autowired private TestRestTemplate restTemplate; @Test void testAddCartItem() { ResponseEntity<String> response = restTemplate.postForEntity("/cart", new Item("Book"), String.class); assertEquals(200, response.getStatusCodeValue()); } } ``` 适合验证端到端流程[^2]。 --- #### 三、最佳实践 1. **测试独立性** 每个测试方法应互不影响,使用`@BeforeEach`或`@AfterEach`重置状态。 2. **模拟外部依赖** 使用`@MockBean`替换数据库、API 等外部服务,避免真实调用。 3. **分层测试策略** - **单元测试**:聚焦单一类(如 Service),使用`@Mock`隔离依赖。 - **集成测试**:使用`@SpringBootTest`验证多组件协作。 4. **断言清晰性** 优先使用 AssertJ 的链式断言: ```java assertThat(user.getName()).isEqualTo("Alice") .startsWith("A"); ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值