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

目录

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

一、什么是单元测试?

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

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


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

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

xml 复制代码
<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 查询用户信息的方法:

java 复制代码
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;
    
    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

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

java 复制代码
@SpringBootTest
public class UserServiceTest {
    
    @Autowired
    private UserService userService;
    
    @MockBean
    private UserRepository userRepository;
    
    @Test
    public void testGetUserById() {
        // 创建一个User对象
        User user = new User();
        user.setId(1L);
        user.setName("ACGkaka");
        user.setEmail("[email protected]");
        
        // 当调用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查询用户信息的接口:

java 复制代码
@RestController
@RequestMapping("/users")
public class UserController {
    
    @Autowired
    private 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);
        }
    }
}

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

java 复制代码
@WebMvcTest(UserController.class)
@AutoConfigureMockMvc
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    public vid testGetUserById() throws Exception {
        // 创建一个 User 对象
        User user = new User();
        user.setId(1L);
        user.setName("ACGkaka");
        user.setEmail("[email protected]");
        
        // 当调用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("[email protected]"));
        
        // 验证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 接口,并提供了一个根据用户姓名查询用户列表的方法:

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

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

java 复制代码
@DataJpaTest
public class UserRepositoryTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private TestEntityManager testEntityManager;
    
    @Test
    public void testFindByName() {
        // 创建两个User对象,并使用testEntityManager.persist方法将其保存到数据库中
        User user1 = new User();
        user1.setName("Bob");
        user1.setEmail("[email protected]");
        testEntityManager.persist(user1);
        
        User user2 = new User();
        user2.setName("Bob");
        user2.setEmail("[email protected]");
        testEntityManager.persist(user2);
        
        // 调用userRepository.findByName()方法,传入"Bob"作为参数,得到一个用户列表。
        List<User> users = userRepository.findByName("Bob");
        
        // 验证用户列表的大小为2,且包含了user1和user2
        assertThat(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 接口,它提供了一个发送邮件的方法:

java 复制代码
public interface EmailService {
    
    void sendEmail(String to, String subject, String content);
}

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

java 复制代码
@SpringBootTest
public class EmailServiceTest {
    
    @Autowired
    private UserService userService;
    
    @MockBean
    private EmailService emailService;
    
    @Test
    public void testSendEmail() {
        // 创建一个User对象
        User user = new User();
        user.setId(1L);
        user.setName("ACGkaka");
        user.setEmail("[email protected]");
        
        // 当调用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 接口,它提供了一个记录日志的方法:

java 复制代码
public interface LogService {
    
    void log(String message);
}

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

java 复制代码
@SpringBootTest
public class LogServiceTest {
 
    @Autowired
    private UserService userService;
 
    @SpyBean
    private LogService logService;
 
    @Test
    public void testLog() {
        // 创建一个User对象
        User user = new User();
        user.setId(1L);
        user.setName("Alice");
        user.setEmail("[email protected]");
 
        // 当调用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

相关推荐
zizisuo1 分钟前
面试篇:Spring Boot
spring boot·面试·职场和发展
pjx9879 分钟前
应用的“体检”与“换装”:精通Spring Boot配置管理与Actuator监控
数据库·spring boot·oracle
络72 小时前
IDEA导入并启动若依项目步骤(SpringBoot+Vue3)
java·spring boot·mysql·vue·intellij-idea
wkj0013 小时前
JDK版本与Spring Boot版本之间对应关系
java·linux·spring boot
littleplayer3 小时前
iOS 单元测试与 UI 测试详解-DeepSeek
前端·单元测试·测试
Andya_net4 小时前
SpringBoot | 构建客户树及其关联关系的设计思路和实践Demo
java·spring boot·后端
axinawang5 小时前
动手试一试 Spring Boot默认缓存管理
spring boot
北漂老男孩5 小时前
Spring Boot 自动配置深度解析:从源码结构到设计哲学
java·spring boot·后端
小咕聊编程6 小时前
【含文档+PPT+源码】基于SpringBoot+Vue的移动台账管理系统
java·spring boot·后端