JUnit5 & MockMvc & Mockito
基于 spring-boot-start 2.2.1.RELEASE 写 Controller 的测试类时,出了好多异常的状况。特将正常运行的代码记录下来,以防遗忘。
依赖版本
使用 Maven 管理的依赖。
- JDK: 1.8
- parent:spring-boot-starter-parent 2.2.1.RELEASE
- dependencies:
- spring-boot-starter-web 2.2.1.RELEASE
- spring-boot-starter-test 2.2.1.RELEASE
测试类注意点
之所以踩了这么多坑,总结下来主要是因为网上的示例大都是基于不同的版本,从而导致写法也各不相同。将各种写法混在一起,又出现了各种奇怪的问题。
另外,我在公司的项目(依赖的包比较多)和我单独写的测试应用中(Spring 包的版本和公司项目是一致的),运行的结果也不一致。
最终的正确写法主要还是参考的官方文档,里面的说明比较详细和准确。
下面是总结的需要注意的地方:
2.2.1.RELEASE 版本中对应 JUnit 的版本是 5.5.2,测试类要使用
@SpringBootTest
注解;很多文档中使用了
@RunWith(SpringRunner.class)
注解,SpringRunner
是 JUnit4 中声明测试类用的。
也不要添加@ExtendWith({SpringExtension.class})
,@SpringBootTest
中已经包含了这个注解。java@SpringBootTest class UserControllerTest { // ... }
如果测试 Controller 接口需要添加
@AutoConfigureMockMvc
注解;使用
@SpringBootTest
注解,默认是不启动 Server 的。
大多数示例中都是在@Before
方法中通过MockMvcBuilders
来创建MockMvc
实例。
另外在 JUnit5 中已经取消了@Before
注解,改为使用@BeforeEach
注解。java@AutoConfigureMockMvc @SpringBootTest class UserControllerTest { @Autowired private MockMvc mockMvc; @Test void login() throws Exception { ResultActions resultActions = this.mockMvc.perform(post("/user/login")); // 解决中文乱码问题 resultActions.andReturn().getResponse().setCharacterEncoding("UTF-8"); resultActions .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string(containsString("OpenId B"))) .andDo(MockMvcResultHandlers.print()); } }
如果需要 Mock 注入的服务等,需要添加
@ExtendWith(MockitoExtension.class)
注解,并且需要添加@MockBean
在对应的字段(测试类中的字段)上;很多文档都是使用的
@Mock
和@IndectMocks
注解配合来实现 Mock 第三方服务的。java@AutoConfigureMockMvc @SpringBootTest @ExtendWith(MockitoExtension.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private WxService wxService; @Test void login() throws Exception { WxLoginResponse wxLoginResponse = new WxLoginResponse(); wxLoginResponse.setOpenId("假用户 OpenId B"); Mockito.when(wxService.login(any(), any(), any(), any())) .thenReturn(wxLoginResponse); // ... } }
测试中曾经出现过的问题:
- 由于 Controller 中使用了
@RequiredArgsConstructor
注解,导致 Mock 始终不起作用;
这个最终实现了使用@RequiredArgsConstructor
注解时仍然可以正常 Mock 。不过当时确实通过改为使用@Autowired
注解实现了 Mock ,不过随之而来的是 Mock 的接口返回值始终是 null 的问题。 - Mock 的服务返回的结果始终是 null;
这个在正式项目和测试项目中运行的结果经常不一致;测试项目中大部分写法都是可以正常返回的,但是正式项目中,不知道什么原因,一致返回的是 null 。
只有通过this.mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
才能注入 Mock 的服务,但最后发现在 JUnit5 是不需要手动创建 mockMvc 实例的,可以简单的通过添加@Autowired
注解来注入。 - Controller 中除了 Mock 的服务,其它需要注入的组件全都是 null;
查到的很多文档中都是通过在被 Mock 的服务字段上添加@Mock
注解,在使用 Mock 服务的组件上添加@InjectMocks
注解。
但是在 JUnit5 中只需要在被 Mock 的服务上添加@MockBean
注解,需要在测试类中定义使用 Mock 服务的组件字段。 - 接口调用时,Filter 没有执行;
之前是通过在 mockMvc 的 builder 处理中添加.addFilter(signAuthFilter)
来实现的,但是之后使用@AutoConfigureMockMvc
注解来注入 mockMvc 实例后,就不需要手动添加 filter 了。
附 1. 完整示例代码
1. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>me.liujiajia</groupId>
<artifactId>junit5-sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>junit5-sample</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. Junit5SampleApplication.java
package me.liujiajia.junit5sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Junit5SampleApplication {
public static void main(String[] args) {
SpringApplication.run(Junit5SampleApplication.class, args);
}
}
3. UserController.java & WxLoginResponse.java
package me.liujiajia.junit5sample.controller;
import lombok.extern.slf4j.Slf4j;
import me.liujiajia.junit5sample.entity.WxLoginResponse;
import me.liujiajia.junit5sample.service.AnotherService;
import me.liujiajia.junit5sample.service.WxService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by 佳佳 on 2021/4/26.
*/
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private WxService wxService;
@Autowired
private AnotherService anotherService;
@PostMapping("login")
public WxLoginResponse login() {
WxLoginResponse wxLoginResponse = wxService.login("appId", "secret", "code", "clientCredential");
return wxLoginResponse;
}
}
package me.liujiajia.junit5sample.entity;
/**
* Created by 佳佳 on 2021/4/26.
*/
public class WxLoginResponse {
private String openId;
public String getOpenId() {
return openId;
}
public void setOpenId(String openId) {
this.openId = openId;
}
}
4. WxService.java & WxServiceImpl.java
package me.liujiajia.junit5sample.service;
import me.liujiajia.junit5sample.entity.WxLoginResponse;
/**
* Created by 佳佳 on 2021/4/26.
*/
public interface WxService {
WxLoginResponse login(String appId, String secret, String code, String clientCredential);
}
package me.liujiajia.junit5sample.service.impl;
import me.liujiajia.junit5sample.entity.WxLoginResponse;
import me.liujiajia.junit5sample.service.WxService;
import org.springframework.stereotype.Service;
/**
* Created by 佳佳 on 2021/4/26.
*/
@Service
public class WxServiceImpl implements WxService {
@Override
public WxLoginResponse login(String appId, String secret, String code, String clientCredential) {
WxLoginResponse wxLoginResponse = new WxLoginResponse();
wxLoginResponse.setOpenId("OpenId A");
return wxLoginResponse;
}
}
5. AnotherService.java & AnotherServiceImpl.java
这个是为了验证之前遇到的服务为空的问题。
package me.liujiajia.junit5sample.service;
public interface AnotherService {
}
package me.liujiajia.junit5sample.service.impl;
import me.liujiajia.junit5sample.service.AnotherService;
import org.springframework.stereotype.Service;
@Service
public class AnotherServiceImpl implements AnotherService {
}
6. UserControllerTest.java
package me.liujiajia.junit5sample;
import me.liujiajia.junit5sample.entity.WxLoginResponse;
import me.liujiajia.junit5sample.service.WxService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
/**
* Created by 佳佳 on 2021/4/26.
*/
@AutoConfigureMockMvc
@SpringBootTest
@ExtendWith(MockitoExtension.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private WxService wxService;
@Test
void login() throws Exception {
WxLoginResponse wxLoginResponse = new WxLoginResponse();
wxLoginResponse.setOpenId("假用户 OpenId B");
Mockito.when(wxService.login(any(), any(), any(), any()))
.thenReturn(wxLoginResponse);
ResultActions resultActions = this.mockMvc.perform(post("/user/login"));
// 解决中文乱码问题
resultActions.andReturn().getResponse().setCharacterEncoding("UTF-8");
resultActions
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string(containsString("OpenId B")))
.andDo(MockMvcResultHandlers.print());
}
}