Springboot的单元测试

前言

在写完代码之后,对我们自己写的代码运行结果不是很确定,此时就需要我们运行项目,发送请求,打断点才能对运行的结果有了结论。那怎样能快速的了解代码运行是否正常,而不需要启动整个项目那么繁琐的步骤呢?单元测试就此应运而生,我们可以为单独的方法去运行,去断言来判断获取结果。

基础介绍

在Springboot的环境下,当然也不例外有组件的配置。我们只需要加入@SpringbootTest注解,就能自动加载整个Springboot应用上下文,使得可以住如何测试各个组件。

添加的依赖

Spring Boot Test Starter(spring-boot-starter-test)包含了 JUnit 5 相关的依赖, Mockito的部分依赖,web测试组件的依赖。

  • junit-,jupiter-api用于编写测试方法和使用 JUnit 5 的注解(如@Test@BeforeEach@AfterEach等)。
  • 可以使用@MockBean注解的Mockito的部分依赖。
  • 包含了spring-webflux-test(如果项目是基于 WebFlux)或者spring-webmvc-test(如果是基于传统的 Spring MVC)等相关依赖,如MockMvc
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

基础应用

java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class UserServiceTest {
    @Autowired
    private UserService userService;
    @Test
    public void testGetUserById() {
        // 调用UserService的方法进行测试
        User user = userService.getUserById(1L);
        assert user!= null;
    }
}

@SpringBootTest注解加载了应用上下文,使得UserService能够被自动注入,然后在测试方法中使用@Test注解标记测试方法,调用UserService的方法并进行断言。

测试类的配置

  • @SpringBootTestclasses 可以指定不同的启动类来使用
  • @SpringBootTestproperties属性和 value属性覆盖配置文件的属性
  • @TestPropertySource 获取资源文件
  • @ActiveProfiles 指定生效的启动配置文件
less 复制代码
@SpringBootTest(classes = DemoApplication.class)
@SpringBootTest(value = "server.port = 8080")
@SpringBootTest(properties = { "server.port = 8080", "my.test = testValue" })
@TestPropertySource(locations = "classpath:test.properties")
@ActiveProfiles("test")

前后置的使用

  • @BeforeAll : 在当前所有测试方法之前执行
  • @BeforeEach : 在每个测试方法之前执行
  • @AfterAll : 在当前所有测试方法之后执行
  • @AfterEach : 在每个测试方法之后执行

对于一些比较耗时的常规的初始化操作且是针对后面测试方法复用的,只需在测试方法开始前执行一次,就可以选用@BeforeAll , 对于每个测试方法需要都一份新的数据,可以使用 @BeforeEach@AfterAll@AfterEach同样同理可得。

java 复制代码
public class DatabaseTest {
    private static List<Connection> connectionPool = new ArrayList<>();
    @BeforeAll
    static void setupDatabaseConnection() throws SQLException {
        // 假设使用MySQL数据库,这里只是示例,实际配置可能更复杂
        String url = "jdbc:mysql://localhost:3306/testdb";
        String user = "root";
        String password = "password";
        for (int i = 0; i < 5; i++) {
            Connection connection = DriverManager.getConnection(url, user, password);
            connectionPool.add(connection);
        }
    }
    @Test
    void testQuery1() {
        // 从连接池中获取连接进行查询操作
        Connection connection = connectionPool.get(0);
        // 执行查询操作的代码,这里省略具体的SQL操作
        //...
    }
    @Test
    void testQuery2() {
        // 从连接池中获取连接进行另一个查询操作
        Connection connection = connectionPool.get(1);
        // 执行查询操作的代码,这里省略具体的SQL操作
        //...
    }
}

Mock操作

在单元测试中,Mock用于创建模拟对象。这些模拟对象可以替代真实的依赖对象,并且可以通过特定的规则(如 Mockito 中的when方法)来定义它们的行为

当出现下述场景的时候,我们通常会使用操作mock。

  • 场景一 : 在Spring的环境下,测试一个类的方法,该方法可能会存在有调用不同其他bean的方法,且其他bean的依赖关系复杂,很难一一去构造。
  • 场景二 : 其他Bean的实现方法还没有完全写好,比如调用数据库的操作,调用第三方依赖的操作。
  • 场景三 : 我们只想测试当前方法,该方法中的其他方法的结果需要构造模拟掉。
typescript 复制代码
@SpringBootTest
public class DemoApplicationTests {

    @Resource
    private TestService testService ;
    @MockBean // 替换掉真实的Bean
    private InnerTestService innerTestService;
    @Test
    public void test() {
        Mockito.when(innerTestService.innerTest()).thenReturn("result");
        String test = testService.test(); //actual 真实值
        Assertions.assertEquals("testresult", test,"数据不一致");
    }
}

上述依赖关系: TestService 依赖 InnerTestService这个bean方法

在使用@MockBean 用于创建一个模拟(Mock)的 Bean,并将其添加到 Spring 应用上下文中,以替换真实的 Bean。 然后再通过Mockito.when方法配置了该bean的行为方法innerTest(),返回特定的结果result

Mockito的常用

Mockito 是一个用于单元测试的 Java 框架,主要用于创建模拟对象(Mock Objects)和验证方法调用

  • 验证行为是否发生 Mockito.verify

验证方法是否被调用以及调用的次数等

scss 复制代码
// 判断是否调用了innertest方法
Mockito.verify(innerTestService).innerTest();
// 判断innerTestService是否调用了innertest方法 两次
Mockito.verify(innerTestService,Mockito.times(2)).innerTest();
  • 模拟对象的行为 when - then
java 复制代码
// 当调用innerTestService.innerTest() 方法默认就直接返回字符串"result"
Mockito.when(innerTestService.innerTest()).thenReturn("result");
  • 参数匹配器**Mockito.anyXX()**

在某些情况下,我们可能不关心方法调用时传入的具体参数值,或者想要使用更灵活的方式来匹配参数。Mockito 提供了参数匹配器来实现这个功能.

go 复制代码
【任何 int 值】  `Mockito.anyInt() `
【任何 long 值 】 `Mockito.anyLong()`
【任何 String 值 】  `Mockito.anyString() `
【任何 XXX 类型的值 等等】` Mockito.any(XXX.class) `
【自定义 argThat的mathes规则实现】`Mockito.argThat(arg -> arg.equals(1) || arg.equals(3)))`

断言Assert

在 Spring Boot 测试中,通常会使用 JUnit 5 提供的Assertions类来进行断言。这个类提供了一系列静态方法,用于验证测试中的条件是否满足预期

  • assertEquals: 判断预期值和实际值是否相等,不想等显示对应的错误信息
  • assertTrueassertFalse : 验证一个布尔表达式是否为真或假
  • assertNullassertNotNull : 验证一个对象是否为null或不为null
  • assertThrows:用于验证一个方法是否抛出了预期的异常
java 复制代码
int result = calculatorService.add(2, 3);
assertEquals(5, result); // 判断result是否等于5

boolean result = numberService.isPositive(5);
assertTrue(result); // 判断result值是否true

User user = userService.getUserById(1L);
assertNotNull(user); // 判断user对象是否为null

// 判断验证该方法是否抛出这个指定的IllegalArgumentException的异常
assertThrows(IllegalArgumentException.class, () -> {
    calculatorService.divide(5, 0);
});

当测试涉及到 JSON 数据(如在测试controller返回的 JSON 内容)时,JSONAssert就比较有用。它用于比较两个 JSON 字符串或者 JSON 对象,验证它们是否在语义上相等

第一种: 比较json对象时,字段顺序无关。当需要验证真实值有额外的扩展字段是否都匹配时,需要开启严格模式strict为true

java 复制代码
String exceptedJson = "{\"name\":\"John\",\"age\":30}";

JSONAssert.assertEquals(exceptedJson, "{\"age\":30,\"name\":\"John\"}", true);//pass
JSONAssert.assertEquals(exceptedJson, "{\"name\":\"John\",\"age\":30,\"score\":90}", false);// pass
JSONAssert.assertEquals(exceptedJson, "{\"name\":\"John\",\"age\":30,\"score\":90}", true);// not pass , 真实值存在额外字段

第二种: 比较json数组的时候,字段必须都匹配,当需要验证字段顺序,需要开启严格模式strict为true

javascript 复制代码
String expectedJsonArray = "[1,2,3,4]";
JSONAssert.assertEquals(expectedJsonArray, "[1,3,2,4]", false); // pass
JSONAssert.assertEquals(expectedJsonArray, "[1,3,2,4]", true); // not pass , strict为true字段顺序验证
JSONAssert.assertEquals(expectedJsonArray, "[1,3,2,4,5]", false); // not pass ,字段没有都匹配
JSONAssert.assertEquals(expectedJsonArray, "[1,3,2,4,5]", true); // not pass , 字段没有都匹配

测试应用场景

在我们平时开发时,会遇到不同的场景需要单独的去测试。对此我们做了如下几种场景分类使用。

Controller控制层

MockMvc是 Spring 框架提供的用于测试 Spring MVC 应用的工具。它允许在不启动完整的 Servlet 容器的情况下,模拟发送 HTTP 请求到Controller,并对Controller返回的响应进行验证

  1. 创建MockMvc的实例 , @WebMvcTest 配置测试环境指定加载controller相关组件。注入MockMvc的bean
kotlin 复制代码
@WebMvcTest(UserController.class)
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
}
  1. 使用MockMvcperform方法来构建和发送请求 ,MockMvcRequestBuilders来构造请求方法
csharp 复制代码
// 构造请求方法,url和参数
MockHttpServletRequestBuilder resquestBuilder = 
                    MockMvcRequestBuilders.get("/hello").param("param", "111");

mockMvc.perform(resquestBuilder);
  1. 验证响应内容的状态码或者结果值是否在预期中

使用了MockMvcResultMatchers的方法status获取请求状态码和和响应结果content,和响应头header。

swift 复制代码
String expectedJson = "{\"id\":1,\"name\":\"John\"}";
mockMvc.perform(resquestBuilder);  
  .andExpect(MockMvcResultMatchers.status().isOk())
  .andExpect(MockMvcResultMatchers.header().string("Content - Type", "application/json"));
  .andExpect(MockMvcResultMatchers.content().json(expectedJson));

4.验证post请求(json/form-data)

同理可得也是构造请求发送请求,无非就是多了请求头上的构造contentType内容和content内容

java 复制代码
@Test
public void testCreateUser() throws Exception {
    String userJson = "{"name":"Anna","id":1}";
    mockMvc.perform(post("/user")
        .contentType(MediaType.APPLICATION_JSON)
        .content(userJson))
    .andExpect(MockMvcResultMatchers.status().isOk());
}

使用form-data发送文件的形式,构建MockMultipartFile类型。图片的来源是使用resources资源文件下的。

less 复制代码
MockMultipartFile mfile = new MockMultipartFile("file", "xx.png", 
            "png", getClass().getClassLoader().getResourceAsStream("images/xx.png"));
mockMvc.perform(MockMvcRequestBuilders.multipart("/testFile").file(mfile))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(result -> Assertions.assertTrue(result.getResponse().getContentAsString().contains("xx.png")));

Service业务逻辑

在业务逻辑层,方法的测试我们一般还是使用 @MockBean,将对应Bean操作转化为自己预设的操作结果。

typescript 复制代码
@SpringBootTest
public class DemoApplicationTests {

    @Resource
    private TestService testService ;
    @MockBean // 替换掉真实的Bean
    private InnerTestService innerTestService;
    @Test
    public void test() {
        Mockito.when(innerTestService.innerTest()).thenReturn("result");
        String test = testService.test(); //actual 真实值
        Assertions.assertEquals("testresult", test,"数据不一致");
    }
}

但对于没有涉及到使用Spring容器的方法测试,我们完全可以进行不用启动整个springboot再来测试,直接运行当方法测试就行。 @Test的注解配合使用 ,无需再使用@SpringBootTest

typescript 复制代码
public class DemoApplicationTests {

    @Test
    public void test() {
        String test = testService.test(); //actual 真实值
        Assertions.assertEquals("testresult", test,"数据不一致");
    }
}

总结

我们日常的单元测试基本就是在Spring环境中对方法进行测试,对方法体内其他的bean操作统一进行mock,模拟出自己的响应,最后判断是否符合自己的预期效果。

相关推荐
番茄Salad42 分钟前
Spring Boot项目中Maven引入依赖常见报错问题解决
spring boot·后端·maven
摇滚侠1 小时前
Spring Boot 3零基础教程,yml配置文件,笔记13
spring boot·redis·笔记
!if2 小时前
springboot mybatisplus 配置SQL日志,但是没有日志输出
spring boot·sql·mybatis
阿挥的编程日记2 小时前
基于SpringBoot的影评管理系统
java·spring boot·后端
java坤坤2 小时前
Spring Boot 集成 SpringDoc OpenAPI(Swagger)实战:从配置到接口文档落地
java·spring boot·后端
摇滚侠3 小时前
Spring Boot 3零基础教程,整合Redis,笔记12
spring boot·redis·笔记
荣淘淘3 小时前
互联网大厂Java求职面试全景实战解析(涵盖Spring Boot、微服务及云原生技术)
java·spring boot·redis·jwt·cloud native·microservices·interview
吃饭最爱3 小时前
spring高级知识概览
spring boot
舒克日记4 小时前
基于springboot针对老年人的景区订票系统
java·spring boot·后端