前言
在写完代码之后,对我们自己写的代码运行结果不是很确定,此时就需要我们运行项目,发送请求,打断点才能对运行的结果有了结论。那怎样能快速的了解代码运行是否正常,而不需要启动整个项目那么繁琐的步骤呢?单元测试就此应运而生,我们可以为单独的方法去运行,去断言来判断获取结果。
基础介绍
在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
的方法并进行断言。
测试类的配置
@SpringBootTest
的classes
可以指定不同的启动类来使用@SpringBootTest
的properties
属性和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
: 判断预期值和实际值是否相等,不想等显示对应的错误信息assertTrue
和assertFalse
: 验证一个布尔表达式是否为真或假assertNull
和assertNotNull
: 验证一个对象是否为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
返回的响应进行验证
- 创建MockMvc的实例 ,
@WebMvcTest
配置测试环境指定加载controller相关组件。注入MockMvc的bean
kotlin
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
}
- 使用
MockMvc
的perform
方法来构建和发送请求 ,MockMvcRequestBuilders
来构造请求方法
csharp
// 构造请求方法,url和参数
MockHttpServletRequestBuilder resquestBuilder =
MockMvcRequestBuilders.get("/hello").param("param", "111");
mockMvc.perform(resquestBuilder);
- 验证响应内容的状态码或者结果值是否在预期中
使用了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,模拟出自己的响应,最后判断是否符合自己的预期效果。