当前位置: 首页 > news >正文

Spring Boot实战(三十六)编写单元测试

目录

    • 一、什么是单元测试?
    • 二、Spring Boot 中的单元测试依赖
    • 三、举例 Spring Boot 中不同层次的单元测试
      • 3.1 Service层
      • 3.2 Controller 层
      • 3.3 Repository层
    • 四、Spring Boot 中 Mock、Spy 对象的使用
      • 4.1 使用Mock对象的背景
      • 4.2 什么是Mock对象,有哪些好处?
      • 4.3 使用 Mock 对象的示例
      • 4.4 什么是Spy对象,有哪些好处?
      • 4.5 使用 Spy 对象的示例

一、什么是单元测试?

单元测试 是指对软件中的最小可测试单元进行检查和验证。在 Java 中,单元测试的最小单元是类。通过编写针对类或方法的小段代码,来检验被测代码是否符合预期结果或行为。

执行单元测试可以帮助开发者验证代码是否正确实现了功能需求,以及是否能够适应应用环境或需求变化。


二、Spring Boot 中的单元测试依赖

在 Spring Boot 项目中,要进行单元测试,首先需要添加相应的依赖。Maven 依赖如下:

<dependency><groupId>org.springframework.boot</groupId><artifactid>spring-boot-starter-test</artifactid><scope>test</scope>
</dependency>

这个依赖包含了多个库和功能,主要有以下几个:

  • JUnit:JUnit 是 Java 中最流行和最常用的单元测试框架,它提供了一套 注解断言 来编写和运行单元测试。例如 @Test 注解表示一个测试方法,assertEquals 断言表示两个值是否相等。
  • Spring Test:Spring Test 是一个基于 Spring 的测试框架,它提供了一套注解和工具来配置和管理 Spring 上下文和 Bean。例如 @SpringBootTest 注解表示一个集成测试类,@Autowired 注解表示自动注入一个 Bean。
  • Mockito:Mockito 是一个 Java 中最流行和最强大的 Mock 对象库,它可以模仿复杂的真实对象行为,从而简化测试过程。例如 @MockBean 注解表示创建一个 Mock 对象,when 方法表示定义 Mock 对象的行为。
  • Hamcrest:Hamcrest 是一个 Java 中的匹配器库,它提供了一套语义丰富而易读的匹配器来进行结果验证。例如 asserThat 断言表示验证一个值是否满足一个匹配器,is 匹配器表示两个值是否相等。
  • AssertJ:AssertJ 是一个 Java 中的断言库,它提供了一套流畅而直观的断言语法来进行结果验证。例如 assertThat 断言表示验证一个值是否满足一个条件,isEqualTo 断言表示两个值是否相等。

除了以上这些库外,spring-boot-starter-test 还包含了其他一些库和功能,如 JsonPath、JsonAssert、XmlUnit 等。这些库和功能可以根据不同的测试场景进行选择和使用。


三、举例 Spring Boot 中不同层次的单元测试

如果是通过spring initialize创建的springboot项目(本系列第一篇文章有讲解),其实会自动创建一个单元测试类:

在这里插入图片描述

我们在写单元测试的时候,直接继承这个类即可。

3.1 Service层

在 Spring Boot 中,对 Service 层进行单元测试,可以使用 @SpringBootTest 注解来加载完整的 Spring 上下文,从而可以自动注入 Service 层的 Bean。同时,可以使用 @MockBean 注解来创建和注入其他层次的 Mock 对象,从而避免真实地调用其他层次的方法,而是模拟其行为。

例如,假设有一个 UserService 类,它提供了一个根据用户 ID 查询用户信息的方法:

@Service
public class UserService {@Autowiredprivate UserRepository userRepository;public User getUserById(Long id) {return userRepository.findById(id).orElse(null);}
}

要对这个类进行单元测试,可以编写以下测试类:

@SpringBootTest
public class UserServiceTest {@Autowiredprivate UserService userService;@MockBeanprivate UserRepository userRepository;@Testpublic void testGetUserById() {// 创建一个User对象User user = new User();user.setId(1L);user.setName("ACGkaka");user.setEmail("acgkaka@example.com");// 当调用userRepository.findById(1L)时,返回一个包含user对象的Optional对象when(userRepository.findById(1L)).thenReturn(Optional.of(user));// 调用userService.getUserId()方法,传入1L作为参数,得到一个User对象。User result = userService.getUserById(1L);// 验证结果对象与user对象相等assertThat(result).isEqualTo(user);// 验证userRepository.findById(1L)方法被调用了一次verify(userRepository, times(1)).findById(1L);}
}

在这个测试类中,使用了以下几个关键点和技巧:

  1. 使用 @SpringBootTest 注解表示加载完成的 Spring 上下文,并使用 @Autowired 注解将 UserService 对象注入到测试类中。
  2. 使用 @MockBean 注解表示创建一个 UserRespository 对象,并使用 @Autowired 注解将其注入到测试类中。这样可以避免真实地调用 UserRepository 的方法,而是模拟其行为。
  3. 使用 when 方法来定义 Mock 对象的行为,例如当调用 userRepository.findById(1L) 时,返回一个包含 user 对象的 Optional 对象。
  4. 使用 userService.getUserById() 方法调用被测方法,得到一个 User 对象。
  5. 使用 AssertJ 的断言语法来验证结果对象与 user 对象是否相等。可以使用多种条件和匹配器来验证结果。
  6. 使用 verify 方法来验证 Mock 对象的方法是否被调用了指定次数。

3.2 Controller 层

Controller 层是指处理用户请求和响应的层,它通常使用 @RestController@Controller 注解来标识。在 Spring Boot 中,对 Controller 层进行单元测试,可以使用 @WebMvcTest 注解来启动一个轻量级的 Spring MVC 上下文,只加载 Controller 层的组件。同时,可以使用 @AutoConfigureMockMvc 注解来自动配置一个 MockMvc 对象,用来模拟 Http 请求和验证 Http 响应。

例如,假设有一个 UserController 类,它提供了一个根据用户ID查询用户信息的接口:

@RestController
@RequestMapping("/users")
public class UserController {@Autowiredprivate UserService userService;@GetMapping("/{id}")public ResponseEntity<User> getUserById(@PathVariable Long id) {User user = userService.getUserById(id);if (user == null) {return ResponseEntity.notFound().build();} else {return ResponseEntity.ok(user);}}
}

要对这个类进行单元测试,可以编写以下测试类:

@WebMvcTest(UserController.class)
@AutoConfigureMockMvc
public class UserControllerTest {@Autowiredprivate MockMvc mockMvc;@MockBeanprivate UserService userService;@Testpublic vid testGetUserById() throws Exception {// 创建一个 User 对象User user = new User();user.setId(1L);user.setName("ACGkaka");user.setEmail("acgkaka@example.com");// 当调用userService.getUserById(1L)时,返回user对象when(userService.getUserById(1L)).thenReturn(user);// 模拟发送GET请求到/users/1,并验证响应状态码为200,响应内容为JSON格式的user对mockMvc.perform(get("/users/1"))mockMvc.perform(get("/users/1")).andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON)).andExpect(jsonPath("$.id").value(1L)).andExpect(jsonPath("$.name").value("ACGkaka")).andExpect(jsonPath("$.email").value("alice@example.com"));// 验证userService.getUserById(1L)方法被调用了一次。verify(userSerivce, times(1)).getUserById(1L);}
}

在这个测试类中,使用了以下几个关键点和技巧:

  1. 使用 @WebMvcTest(UserController.class) 注解表示只加载 UserController 类的组件,不加载其他层次的组件。
  2. 使用 @AutoConfigureMockMvc 注解表示自动配置一个 MockMvc 对象,并使用 @Autowired 注解将其注入到测试类中。
  3. 使用 @MockBean 注解表示创建一个 UserService 的 Mock 对象,并使用 @Autowired 注解将其注入到测试类中。这样可以避免真实地调用 UserService 的方法,而是模拟其行为。
  4. 使用 when() 方法来定义 Mock 对象的行为,例如当调用 userService.getUserById(1L) 时,返回 user 对象。
  5. 使用 mockMvc.perform() 方法来模拟发送 Http 请求,并使用 andExpect 方法来验证 Http 响应。可以使用多种匹配器来验证响应状态码、内容类型、内容值等。
  6. 使用 verify() 方法来验证 Mock 对象的方法是否被调用了指定次数。

3.3 Repository层

在 Spring Boot 中,对 Repository 层进行单元测试,可以使用 @DataJpaTest 注解来启动一个嵌入式数据库,并自动配置 JPA 相关的组件。同时,可以使用 @TestEntityManager 注解来获取一个 TestEntityManager 对象,用来操作和验证数据库数据。

例如,假设有一个 UserRepository 接口,它继承了 JpaRepository 接口,并提供了一个根据用户姓名查询用户列表的方法:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {List<User> findByName(String name);
}

要对这个接口进行单元测试,可以编写以下测试类:

@DataJpaTest
public class UserRepositoryTest {@Autowiredprivate UserRepository userRepository;@Autowiredprivate TestEntityManager testEntityManager;@Testpublic void testFindByName() {// 创建两个User对象,并使用testEntityManager.persist方法将其保存到数据库中User user1 = new User();user1.setName("Bob");user1.setEmail("bob@example.com");testEntityManager.persist(user1);User user2 = new User();user2.setName("Bob");user2.setEmail("bob2@example.com");testEntityManager.persist(user2);// 调用userRepository.findByName()方法,传入“Bob”作为参数,得到一个用户列表。List<User> users = userRepository.findByName("Bob");// 验证用户列表的大小为2,且包含了user1和user2assertThat(users).hasSize(2);assertThat(users).contains(user1, user2);}
}

在这个测试类中,使用了以下几个关键点和技巧:

  1. 使用 @DataJpaTest 注解表示启动一个嵌入式数据库,并自动配置 JPA 相关的组件。这样可以避免依赖外部数据库,而是使用内存数据库进行测试。
  2. 使用 @Autowired 注解将 UserRepository 和 TestEntityManager 对象注入到测试类中。
  3. 使用 testEntityManager.persist() 方法将 User 对象保存到数据库中。这样可以准备好测试数据,而不需要手动插入数据。
  4. 使用 userRepository.findByName() 方法调用自定义的查询方法,得到一个用户列表。
  5. 使用 AssertJ 的断言语法来验证用户列表的大小和内容。可以使用多种条件和匹配器来验证结果。

四、Spring Boot 中 Mock、Spy 对象的使用

4.1 使用Mock对象的背景

在 Spring Boot 中,除了使用 @WebMvcTest 和 @DataJpaTest 等注解来加载特定层次的组件外,还可以使用 @SpringBootTest 注解来加载完整的 Spring 上下文,从而进行更加集成的测试。但是,在这种情况下,可能会遇到一些问题,例如:

  • 测试过程中需要依赖外部资源,如:数据库、消息队列、Web服务等。这些资源可能不稳定或不可用,导致测试失败或超时。
  • 测试过程中需要调用其他组件或服务的方法,但是这些方法的实现或行为不确定或不可控,导致测试结果不可预测或不准确。
  • 测试过程中需要验证一些难以观察或测量的结果,如:日志输出、异常抛出、私有变量值等。这些结果可能需要使用复杂或侵入式的方式来获取或验证。

为了解决这些问题,可以使用 Mock 对象来模拟真实对象行为。

4.2 什么是Mock对象,有哪些好处?

Mock 对象是指在测试过程中替代真实对象的虚拟对象,它可以根据预设的规则来返回特定的值或执行特定的操作。使用 Mock 对象有以下好处:

  • 降低测试依赖: 通过使用 Mock 对象来替代外部资源或其他组件,可以减少测试过程中对真实环境的依赖,使得测试更加稳定和可靠。
  • 提高测试控制: 通过使用 Mock 对象来模拟特定的行为或场景,可以提高测试过程中对真实对象行为的控制,使得测试更加灵活和精确。
  • 简化测试验证: 通过使用 Mock 对象来返回特定的结果或触发特定的事件,可以简化测试过程中对真实对象结果或事件的验证,使得测试更加简单和直观。

4.3 使用 Mock 对象的示例

在 Spring Boot 中,要使用 Mock 对象,可以使用 @MockBean 注解来创建和注入一个 Mock 对象。这个注解会自动使用 Mockito 库来创建一个 Mock 对象,并将其添加到 Spring 上下文中。同时,可以使用 when() 方法来定义 Mock 对象的行为,以及 verify() 方法来验证 Mock 对象的方法调用。

例如,假设有一个 EmailService 接口,它提供了一个发送邮件的方法:

public interface EmailService {void sendEmail(String to, String subject, String content);
}

要对这个接口进行单元测试,可以编写以下测试类:

@SpringBootTest
public class EmailServiceTest {@Autowiredprivate UserService userService;@MockBeanprivate EmailService emailService;@Testpublic void testSendEmail() {// 创建一个User对象User user = new User();user.setId(1L);user.setName("ACGkaka");user.setEmail("acgkaka@example.com");// 当调用emailService.sendEmail方法时,什么也不做doNothing().when(emailService).sendEmail(anyString(), anyString(), anyString());// 调用userService.sendWelcomeEmail方法,传入user对象作为参数userService.sendWelcomeEmail(user);// 验证emailService.sendEmail方法被调用了一次,并且参数分别为user.getEmail()、"Welcome"、"Hello, ACGkaka"verify(emailService, times(1)).sendEmail(user.getEmail(), "Welcom", "Hello, ACGkaka");}
}

在这个测试类中,使用了以下几个关键点和技巧:

  1. 使用 @SpringBootTest 注解表示加载完整的 Spring 上下文,并使用 @Autowired 注解将 UserService 对象注入到测试类中。
  2. 使用 @MockBean 注解表示创建一个 EmailService 的 Mock 对象,并使用 @Autowired 注解将其注入到测试类中。这样可以避免真实地调用 EmailService 的方法,而是模拟其行为。
  3. 使用 doNothing() 方法来定义 Mock 对象的行为,例如当调用 emailService.sendEmail() 方法时,什么也不做。也可以使用 doReturn()、doThrow()、doAnswer() 等方法来定义其他类型的行为。
  4. 使用 anyString() 方法来表示任意字符串类型的参数,也可以使用 anyInt、anyLong、anyObject 等方法来表示其他类型的参数。
  5. 使用 userService.sendEmail() 方法调用被测方法,传入user对象作为参数。
  6. 使用 verify() 方法来验证 Mock 对象的方法是否被调用了指定次数,并且参数是否符合预期。也可以使用 never、atLeast、atMost 等方法来表示其他次数的验证。

4.4 什么是Spy对象,有哪些好处?

除了使用 @MockBean 注解来创建和注入 Mock 对象外,还可以使用 @SpyBean 注解来创建和注入 Spy 对象。Spy 对象是指在测试u工程中部分替代真实对象的虚拟对象,它可以根据预设的规则来返回特定的值或执行特定的操作,同时保留真实对象的其他行为。使用 Spy 对象有以下好处:

  • 保留真实行为: 通过使用 Spy 对象来替代真实对象,可以保留真实对象的其他行为,使得测试更加接近真实环境。
  • 修改部分行为: 通过使用 Spy 对象来模拟特定的行为或场景,可以修改真实对象的部分行为,使得测试更加灵活和精确。
  • 观察真实结果: 通过使用 Spy 对象来返回特定的结果或触发特定的事件,可以观察真实对象的结果或事件,使得测试更加直观和可信。

4.5 使用 Spy 对象的示例

在 Spring Boot 中,要使用 Spy 对象,可以使用 @SpyBean 注解来创建和注入一个 Spy 对象。这个注解会自动使用 Mockito 库来创建一个 Spy 对象,并将其添加到 Spring 上下文中。同时,可以使用 when() 方法来定义 Spy 对象的行为,以及 verify() 方法来验证 Spy 对象的方法调用。

例如,假设有一个 LogService 接口,它提供了一个记录日志的方法:

public interface LogService {void log(String message);
}

要对这个接口进行单元测试,可以编写以下测试类:

@SpringBootTest
public class LogServiceTest {@Autowiredprivate UserService userService;@SpyBeanprivate LogService logService;@Testpublic void testLog() {// 创建一个User对象User user = new User();user.setId(1L);user.setName("Alice");user.setEmail("alice@example.com");// 当调用logService.log方法时,调用真实的方法,并打印参数到控制台doAnswer(invocation -> {String message = invocation.getArgument(0);System.out.println(message);invocation.callRealMethod();return null;}).when(logService).log(anyString());// 调用userService.createUser方法,传入user对象作为参数userService.createUser(user);// 验证logService.log方法被调用了两次,并且参数分别为"Creating user: Alice"、"User created: Alice"verify(logService, times(2)).log(anyString());verify(logService, times(1)).log("Creating user: Alice");verify(logService, times(1)).log("User created: Alice");}
}

在这个测试类中,使用了以下几个关键点和技巧:

  1. 使用 @SpringBootTest 注解表示加载完整的Spring上下文,并使用@Autowired注解将UserService对象注入到测试类中。
  2. 使用 @SpyBean 注解表示创建一个LogService的Spy对象,并使用@Autowired注解将其注入到测试类中。这样可以保留LogService的真实行为,同时修改部分行为。
  3. 使用 doAnswer() 方法来定义Spy对象的行为,例如当调用logService.log方法时,调用真实的方法,并打印参数到控制台。也可以使用doReturn、doThrow、doNothing等方法来定义其他类型的行为。
  4. 使用 anyString() 方法来表示任意字符串类型的参数。也可以使用anyInt、anyLong、anyObject等方法来表示其他类型的参数。
  5. 使用 userService.createUser() 方法调用被测方法,传入user对象作为参数。
  6. 使用 verify() 方法来验证Spy对象的方法是否被调用了指定次数,并且参数是否符合预期。也可以使用never()、atLeast()、atMost() 等方法来表示其他次数的验证。

整理完毕,完结撒花~🌻





参考地址:

1.Spring Boot中如何编写优雅的单元测试,https://blog.csdn.net/TaloyerG/article/details/132487310

2.【快学springboot】在springboot中写单元测试,https://cloud.tencent.com/developer/article/2385462

相关文章:

  • matlab 绘图
  • 手搓雷达图(MATLAB)
  • 网络安全 | F5 WAF 黑白名单配置实践指南
  • ArcGIS Pro跨图层复制粘贴
  • 第十三届蓝桥杯 2022 C/C++组 修剪灌木
  • 抖音的逆向工程获取弹幕(websocket和protobuf解析)
  • 【QT网络】构建简单Udp回显服务器
  • Flutter Dart 循环语句 for while do..while break、continue
  • CGAL 网格内部生成随机点
  • 图论---朴素Prim(稠密图)
  • Linux内核netlink机制 - 连接器(Netlink Connector)
  • 解决cannot find attribute `serde` in this scope记录
  • 远程访问服务器的Jupyter Notebook
  • 生成随机验证码-解析与优化
  • 代码随想录算法训练营第一天:数组part1
  • 第六章 QT基础:6、QT的Qt 时钟编程
  • 协作开发攻略:Git全面使用指南 — 第三部分 特殊应用场景
  • JW01三合一传感器详解(STM32)
  • 深度剖析操作系统核心(第一节):从X86/ARM/MIPS处理器架构到虚拟内存、分段分页、Linux内存管理,再揭秘进程线程限制与优化秘籍,助你成为OS高手!
  • Ubuntu 一站式部署 RabbitMQ 4 并“彻底”迁移数据目录的终极实践
  • 冯象|那“交出”后的崩溃,如撒旦坠落诸天
  • 广东东莞调整普通住宅价格标准:一类镇街上浮300余元/平方米
  • 央行上海总部:受益于过境免签政策,上海市外卡刷卡支付交易量稳步增长
  • 委托第三方可一次性补缴十多万元的多年社保?广州多人涉嫌被骗后报警
  • 调查显示特朗普在经济问题上的支持率跌至其总统生涯最低
  • 对话|棋后居文君:创造历史之后,还有继续追梦的心