• 作者:老汪软件技巧
  • 发表时间:2024-11-18 10:04
  • 浏览量:

前言

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

基础介绍

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

添加的依赖

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

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-testartifactId>
    <scope>testscope>
dependency>

基础应用

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的方法并进行断言。

测试类的配置

@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同样同理可得。

public class DatabaseTest {
    private static List 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。

@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)和验证方法调用

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

// 判断是否调用了innertest方法
Mockito.verify(innerTestService).innerTest();
// 判断innerTestService是否调用了innertest方法 两次
Mockito.verify(innerTestService,Mockito.times(2)).innerTest();

// 当调用innerTestService.innerTest() 方法默认就直接返回字符串"result"
Mockito.when(innerTestService.innerTest()).thenReturn("result");

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

【任何 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类来进行断言。这个类提供了一系列静态方法,用于验证测试中的条件是否满足预期


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

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

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返回的响应进行验证

创建MockMvc的实例 , @WebMvcTest 配置测试环境指定加载controller相关组件。注入MockMvc的bean

@WebMvcTest(UserController.class)
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
}

使用MockMvc的perform方法来构建和发送请求 ,MockMvcRequestBuilders来构造请求方法

// 构造请求方法,url和参数
MockHttpServletRequestBuilder resquestBuilder = 
                    MockMvcRequestBuilders.get("/hello").param("param", "111");
mockMvc.perform(resquestBuilder);

验证响应内容的状态码或者结果值是否在预期中

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

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内容

@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资源文件下的。

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操作转化为自己预设的操作结果。

@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

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

总结

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


上一条查看详情 +函数式编程思想
下一条 查看详情 +没有了