1.环境准备
本文主要是介绍mock测试方面的知识,用到的环境是 idea + jdk8 + mysql5.5.49 + Junit5 + springBoot 2.6 + mokito
1.1数据库脚本准备
-- 创建订单表
CREATE TABLE `sale_order` (
`id` bigint NOT null AUTO_INCREMENT COMMENT '客户订单ID',
`sale_order_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '订单编号',
`customer` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '客户',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uni_sale_order_code` (`sale_order_code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='客户订单(销售订单)';
-- 创建订单颜色明细表
CREATE TABLE `sale_order_detail` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '客户订单明细ID',
`sale_order_id` bigint NOT NULL COMMENT '客户订单ID',
`color` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL,
`quantity` bigint DEFAULT NULL COMMENT '数量',
`receiver_price` decimal(22,2) DEFAULT NULL COMMENT '接单价格',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='客户订单明细表(下单信息)';
1.2.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.6.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.fufulong.demo</groupId>
<artifactId>mock-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mock-demo</name>
<description>mock-demo</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.12.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
<include>**/*.yml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>META-INF/**</include>
<include>template/*</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
1.3 配置文件准备
spring:
application:
name: mock-demo
datasource:
username: root
password: xxxx
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=CONVERT_TO_NULL&useSSL=false&rewriteBatchedStatements=true
servlet:
multipart:
max-request-size: 100MB
max-file-size: 10MB
server:
port: 9000
servlet:
context-path: /mock-demo
2. mockMvc 的使用方法
2.1 初始化mockMvc
使用@SpringBootTest
注解会加载整个Context。这样我们可以自动获得所有在Context中注入的Bean,以及从application.properties中加载的配置信息。
在@SpringBootTest
中声明webEnvironment为WebEnvironment.MOCK
(默认值就是WebEnvironment.MOCK
)后,结合@AutoConfigureMockMvc
注解,在测试的时候会得到一个模拟的Web/Servlet环境。
因为没有Web Server,所以就无法使用RestTemplate
,也就只能继续使用MockMVC
了。这次MockMVC
的实例是由@AutoConfigureMockMvc
注解来完成的。这归功于SpringBoot的自动化配置。
junit5和junit4环境,初始化mocKMvc的方法不一样,如果是Junit5,初始化 mockMvc的代码如下:
@SpringBootTest(classes = MockDemoApplication.class)
@AutoConfigureMockMvc
class MockDemoApplicationTests {
@Autowired
private MockMvc mockMvc;
}
也可以使用另外一种方式,使用MockMvcBuilders 得静态方法初始化mockMvc,这样做还可以设置每次mock测试都要执行的动作,期望等
@SpringBootTest(classes = MockDemoApplication.class)
class MockDemoApplicationTests {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext webApplicationContext;
@BeforeEach
public void setUp(){
// 设定mockMvc能mock的controller是整个springBoot项目环境中的controller
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext). build();
}
}
2.2 测试post类型的接口
接口方法
@RestController
@RequestMapping("saleOrder")
public class SaleOrderController {
@Autowired
private SaleOrderService saleOrderService;
@PostMapping(value = "/save")
public DataResponse<Long> save(@RequestBody SaveSaleOrderReq req){
Long id = saleOrderService.save(req);
return DataResponse.ok(id);
}
}
测试方法
@Test
public void test1() throws Exception {
SaleOrder saveOrder = new SaleOrder();
saveOrder.setSaleOrderCode("0001");
saveOrder.setCustomer("小明");
MockHttpServletRequestBuilder requestBuilder =
//请求路径,不需要带前面 servletContext部分
MockMvcRequestBuilders.post("/saleOrder/save")
// post请求返回的mediaType
.accept(MediaType.APPLICATION_JSON)
// post请求的请求内容的 mediaType
.contentType(MediaType.APPLICATION_JSON)
// post 请求参数内容
.content(JSON.toJSONString(saveOrder));
MvcResult mvcResult = mockMvc.perform(requestBuilder)
// 设置期望的结果,用 ResultMatcher 来表示
.andExpect(MockMvcResultMatchers.status().isOk())
// 内置的打印mock请求结果
.andDo(MockMvcResultHandlers.print())
// 正式执行接口,并返回接口的返回值
.andReturn();
// 把 mvcResult 的 response对象,转换成 json,然后打印显示
String s = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
System.out.println(s);
}
结果如下所示:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /saleOrder/save
Parameters = {}
Headers = [Content-Type:"application/json", Accept:"application/json", Content-Length:"44"]
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = com.fufulong.demo.mockdemo.controller.SaleOrderController
Method = com.fufulong.demo.mockdemo.controller.SaleOrderController#save(SaveSaleOrderReq)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"successful":true,"code":"200","message":null,"data":1}
Forwarded URL = null
Redirected URL = null
Cookies = []
{"successful":true,"code":"200","message":null,"data":1}
2.3 测试get类型的接口 (requestParam参数类型)
接口方法
@GetMapping(value = "/get-one")
public DataResponse<SaleOrder> getOne(@RequestParam(value = "saleOrderId") Long saleOrderId){
SaleOrder saleOrder = saleOrderService.selectOne(saleOrderId);
return DataResponse.ok(saleOrder);
}
测试方法
@Test
public void test2() throws Exception {
MockHttpServletRequestBuilder requestBuilder =
//请求路径,不需要带前面 servletContext部分
MockMvcRequestBuilders.get("/saleOrder/get-one")
// 设置请求参数
.param("saleOrderId","1");
MvcResult mvcResult = mockMvc.perform(requestBuilder)
// 设置期望的结果,用 ResultMatcher 来表示
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn();
String s = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
System.out.println(s);
}
测试结果输出
MockHttpServletRequest:
HTTP Method = GET
Request URI = /saleOrder/get-one
Parameters = {saleOrderId=[1]}
Headers = []
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = com.fufulong.demo.mockdemo.controller.SaleOrderController
Method = com.fufulong.demo.mockdemo.controller.SaleOrderController#getOne(Long)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"successful":true,"code":"200","message":null,"data":{"id":1,"saleOrderCode":"0001","customer":"å°æ˜Ž"}}
Forwarded URL = null
Redirected URL = null
Cookies = []
{"successful":true,"code":"200","message":null,"data":{"id":1,"saleOrderCode":"0001","customer":"小明"}}
2.4 测试get类型的接口 (path参数类型)
接口代码
@GetMapping(value = "/get-one/{saleOrderCode}/{id}")
public DataResponse<SaleOrder> getOneByCode(@PathVariable(value = "saleOrderCode") String saleOrderCode,
@PathVariable(value = "id") Long id ){
SaleOrder saleOrder = saleOrderService.getOneByCode(saleOrderCode,id);
return DataResponse.ok(saleOrder);
}
测试方法
@Test
public void test3() throws Exception {
String saleOrderCode = "0001";
Long id = 1L;
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/saleOrder/get-one/{saleOrderCode}/{id}",saleOrderCode,id);
MvcResult mvcResult = mockMvc.perform(requestBuilder)
// 设置期望的结果,用 ResultMatcher 来表示
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn();
String s = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
System.out.println(s);
}
测试结果打印
MockHttpServletRequest:
HTTP Method = GET
Request URI = /saleOrder/get-one/0001/1
Parameters = {}
Headers = []
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = com.fufulong.demo.mockdemo.controller.SaleOrderController
Method = com.fufulong.demo.mockdemo.controller.SaleOrderController#getOneByCode(String, Long)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"successful":true,"code":"200","message":null,"data":{"id":1,"saleOrderCode":"0001","customer":"å°æ˜Ž"}}
Forwarded URL = null
Redirected URL = null
Cookies = []
{"successful":true,"code":"200","message":null,"data":{"id":1,"saleOrderCode":"0001","customer":"小明"}}
2.5 测试上传文件
接口代码
@RequestMapping(value = "/uploadFile")
public DataResponse<Void> uploadFile(@RequestParam(value = "file") MultipartFile file) throws IOException {
InputStream inputStream = null;
OutputStream outputStream = null;
try{
inputStream = file.getInputStream();
outputStream = FileUtil.getOutputStream(new File(Objects.requireNonNull(file.getOriginalFilename())));
IoUtil.copy(inputStream,outputStream);
}finally {
if (inputStream != null){
inputStream.close();
}
if (outputStream != null){
outputStream.close();
}
}
return DataResponse.ok();
}
测试代码
@Test
public void test4() throws Exception {
File file = new File("C:\\Users\\付福龙\\Desktop\\兔子.jpeg");
MockMultipartFile uploadFile = new MockMultipartFile("file", "兔子.jpeg", MediaType.IMAGE_JPEG_VALUE, new FileInputStream(file));
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.multipart("/saleOrder/uploadFile").file(uploadFile).contentType( MediaType.IMAGE_JPEG);
MvcResult mvcResult = mockMvc.perform(requestBuilder)
// 设置期望的结果,用 ResultMatcher 来表示
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn();
}
测试结果
MockHttpServletRequest:
HTTP Method = POST
Request URI = /saleOrder/uploadFile
Parameters = {}
Headers = [Content-Type:"image/jpeg"]
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = com.fufulong.demo.mockdemo.controller.SaleOrderController
Method = com.fufulong.demo.mockdemo.controller.SaleOrderController#uploadFile(MultipartFile)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"successful":true,"code":"200","message":null,"data":null}
Forwarded URL = null
Redirected URL = null
Cookies = []
3.mockMvc测试controller总结
- 先有业务接口方法
- 新建的测试类中,需要先初始化mockMvc对象
- 根据接口方法的类型不同(post/get/multipart等),选择构建不同的MockHttpServletRequestBuilder 对象, 在这里设置请求地址,注意地址是不需要 地址端口和servletContextpath前缀的,且需要注意用"/"开头. 然后设置 queryParam或者pathParam或者 content,还可以设置请求头等
- 调用mockMvc的方法,指定需要执行的请求, mockMvc.perform(MockHttpServletRequestBuilder )
- 然后设置断言,断言可以设置多个,使用方法 andExpect(ResultMatcher matcher) 设置, MockMvcResultMatchers中有许多常用的断言结果匹配器,可以使用
- 然后设置结果处理方法, 使用方法 ResultActions andDo(ResultHandler handler) 来处理,同样 MockMvcResultHandlers 总有需要封装好的静态方法可以用
- 然后 调用方法 MvcResult andReturn() ,此时才会真的执行接口, 得到 MvcResult 结果
- 可以使用 mvcResult.getResponse() 得到 MockHttpServletResponse 对象, 这个对象有许多请求结果的信息,比如状态,内容等, 可以根据这些内容写后续的验证代码
4. mockMvc测试重要API
41. MockMvcRequestBuilders
- MockMvcRequestBuildersMockHttpServletRequestBuilder get(String urlTemplate, Object… uriVars)
构建 get请求,设置请求地址,并且可以设置路径参数,注意如果要设置路径参数,参数值和类型必须要对应地址中参数名 - MockHttpServletRequestBuilder post(String urlTemplate, Object… uriVars)
构建 post 请求, 设置请求地址, post方法虽然也可以设置 pathParam和queryParam,但是不建议这样做. - MockMultipartHttpServletRequestBuilder multipart(String urlTemplate, Object… uriVars)
构建 上传文件 请求, 设置上传文件的接口地址和路径参数等.
4.2 MockMvcRequestBuilder
-
MockHttpServletRequestBuilder param(String name, String… values)
向请求参数map中添加非path类型的参数,也就是设置 queryParam的方法
-
MockHttpServletRequestBuilder header(String name, Object… values)
向请求头map中添加请求头键值对 -
MockHttpServletRequestBuilder contentType(MediaType contentType)
设置接口的参数内容的 contentType,如果业务接口是 @requestController 修饰的, contentType 一般是MediaType.APPLICATION_JSON -
MockHttpServletRequestBuilder accept(MediaType… mediaTypes)
设置接口的可以接收的返回数据的MediaType, 如果业务接口是 @requestController 修饰的, 一般是MediaType.APPLICATION_JSON -
MockHttpServletRequestBuilder content(String content)
设置requestBody的内容,一般是post请求的时候需要设置
4.3 ResultActions
- ResultActions andExpect(ResultMatcher matcher)
设置请求相关的断言,可以连续调用多次设置不同的断言. - ResultActions andDo(ResultHandler handler)
设置对接口返回结果的处理方式 - MvcResult andReturn()
执行测试接口,得到结果,返回数据
4.4 MockMvcResultMatchers
- StatusResultMatchers status(): 得到结果状态匹配器, 可以使用StatusResultMatchers 的方法断言请求状态,请求是否正常返回等.
- ModelResultMatchers model(): 当请求不是rest风格的时候,返回接口结果中的数据model,可用进一步通过调用 ModelResultMatchers 的静态方法,判断返回的数据中的属性的值等
- ViewResultMatchers view(): 当请求不是rest风格的时候,返回接口结果中的视图地址匹配器
- JsonPathResultMatchers jsonPath(String expression, Object… args) / ResultMatcher jsonPath(String expression, Matcher<? super T> matcher) / ResultMatcher jsonPath(String expression, Matcher<? super T> matcher, Class targetType)
当接口返回的数据是Json的形式,可以用这3个方法设置断言匹配器, 关于 jsoPath的设置,比较复杂,可以看官网地址: https://github.com/json-path/JsonPath
4.5 MockMvcResultHandlers
这里的方法主要是打印或者日志记录mock测试的主要信息,注意如果是打印到系统的控制台,使用的是系统的编码方式,如果是wIndows系统,就是ISO-8859-1,那么输出的中文就是乱码了.
4.6 MockHttpServletResponse
- String getContentAsString(Charset fallbackCharset)
把接口返回的数据内容转换成字符串形式,并指定了编码方式