目录
5 @AutoConfigureMockMvc,MockMvc
1. 框架:
springboot + mybatis + mysql + junit5
2. 项目代码
2.1 pom文件
<?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.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>demo</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- springboot框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.5</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.15</version>
</dependency>
<!-- jsbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.6</version>
</dependency>
<!-- 事务 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<!-- springboot自带的测试包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--springboot版本2.2以上用的junit5,不处理的情况下,代码会自动使用junit4,避免版本冲突影响mvn test,故单独处理-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!--maven插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--命令:mvn test 生成xml txt测试报告 (maven-surefire-plugin mvn默认使用插件,可以不用配置) -->
<!--命令:mvn surefire-report:report 生成测试报告html -->
<!--junit5 下方插件的version要大于2.22.0,否则无法执行test-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<showSuccess>false</showSuccess>
</configuration>
</plugin>
<!--命令:mvn cobertura:cobertura 生成测试覆盖率报告html -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.5.1</version>
</plugin>
</plugins>
</build>
</project>
2.2 spring配置文件
# Tomcat
server:
port: 8080
# spring配置
spring:
profiles:
# 环境配置
active: dev
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/product?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
# mybatis配置
mybatis:
#搜索指定包别名
typeAliasesPackage: com.example.demo.entity
#配置mapper的扫描,找到所有的mapper.xml映射文件
mapperLocations: classpath:mapper/*.xml
configLocations: classpath:mybatis-config.xml
2.3 Controller层
package com.example.demo.controller;
import com.example.demo.entity.Product;
import com.example.demo.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @Author mxy
* @Date 2021/10/13
* @Desc 控制器
*/
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping()
public List<Product> list(Product product) {
return productService.queryAll(product);
}
@PutMapping()
public Boolean update(@RequestBody Product product) {
return productService.update(product);
}
@DeleteMapping("/{id}")
public Boolean deleteById(@PathVariable String id) {
return productService.deleteById(id);
}
@PostMapping()
public Product save(@RequestBody Product product) {
return productService.insert(product);
}
}
2.4 实体类
package com.example.demo.entity;
/**
* @Author mxy
* @Date 2021/9/1
* @Desc 实体
*/
public class Product {
/** 对象ID */
private String id;
/** 产品名称 */
private String name;
/** 产品报价 */
private Double price;
/** 是否启用 */
private Integer status;
public Product() {
}
public Product(String id, String name, Double price, Integer status) {
this.id = id;
this.name = name;
this.price = price;
this.status = status;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
2.5 Service服务类
package com.example.demo.service;
import com.example.demo.entity.Product;
import java.util.List;
/**
* @Author mxy
* @Date 2021/10/13
* @Desc 服务类
*/
public interface ProductService {
/**
* 查询所有数据
*
* @param product 筛选条件
* @return
*/
List<Product> queryAll(Product product);
/**
* 根据ID查询数据详情
*
* @param id
* @return
*/
Product queryById(String id);
/**
* 根据ID作废某数据(非物理删除)
*
* @param id 对象ID
* @return
*/
Boolean deleteById(String id);
/**
* 保存数据
*
* @param product 实体
* @return
*/
Product insert(Product product);
/**
* 更新某一条数据
*
* @param product 实体
* @return
*/
Boolean update(Product product);
}
2.6 服务实现类
package com.example.demo.service.impl;
import com.example.demo.entity.Product;
import com.example.demo.mapper.ProductMapper;
import com.example.demo.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
/**
* @Author mxy
* @Date 2021/10/13
* @Desc 服务实现类
*/
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductMapper productMapper;
@Override
public List<Product> queryAll(Product product) {
return productMapper.selectList(product);
}
@Override
public Product queryById(String id) {
return productMapper.selectById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteById(String id) {
Product product = new Product();
product.setStatus(0);
product.setId(id);
return productMapper.updateById(product) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Product insert(Product product) {
product.setId(UUID.randomUUID().toString());
product.setStatus(1);
productMapper.insert(product);
return product;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean update(Product product) {
return productMapper.updateById(product) > 0;
}
}
2.7 mapper接口
package com.example.demo.mapper;
import com.example.demo.entity.Product;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* Mapper 接口
*
* @author mxy
* @since 2021-09-30
*/
@Mapper
public interface ProductMapper {
List<Product> selectList(Product product);
Product selectById(String id);
int updateById(Product product);
int insert(Product product);
}
2.8 启动类
package com.example.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @Author mxy
* @Date 2021/10/13
* @Desc 启动类
*/
@SpringBootApplication
@MapperScan(basePackages = {"com.example.demo.mapper"})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
3 测试类代码
3.1 Controller 集成测试
package com.example.demo.controller;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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.http.MediaType;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.transaction.annotation.Transactional;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @Author mxy
* @Date 2021/10/13
* @Desc 集成测试
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
public class ProductControllerTest {
private static final String BASE_URL = "/product";
@Autowired
private MockMvc mockMvc;
@Test
@Transactional
@Rollback()
public void save() throws Exception {
String json = "{\"name\":\"testOne\", \"price\":100}";
mockMvc.perform(MockMvcRequestBuilders
.post(BASE_URL)
.accept(MediaType.APPLICATION_JSON_VALUE)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(json.getBytes()))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("testOne"))
.andDo(print())
.andReturn(); //返回MvcResult
}
/*
* 1、mockMvc.perform执行一个请求。
* 2、MockMvcRequestBuilders.get("XXX")构造一个请求。
* 3、ResultActions.param添加请求传值
* 4、ResultActions.accept(MediaType.TEXT_HTML_VALUE))设置返回类型
* 5、ResultActions.andExpect添加执行完成后的断言。
* 6、ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情
* 比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。
* 7、ResultActions.andReturn表示执行完成后返回相应的结果,可根据结果进行下一步操作。
*/
@Test
@Transactional
@Rollback()
public void list() throws Exception {
//这里测试list接口,andExpect表示期望返回的值,如果是期望值会报异常
mockMvc.perform(MockMvcRequestBuilders
.get(BASE_URL)
.accept(MediaType.APPLICATION_JSON_VALUE)
.param("name", "testOne")
// .header("Authorization", "Bearer ********-****-****-****-************")
)
.andExpect(status().isOk())
// .andExpect(MockMvcResultMatchers.content().string("成功"));
.andDo(print());
// .andReturn();
}
@Test
@Transactional
@Rollback()
public void update() throws Exception {
String json = "{\"id\":\"1dd85201-3d91-4b8d-94d1-42e8d33a71b4\",\"name\":\"testOne-update\"}";
mockMvc.perform(MockMvcRequestBuilders
.post(BASE_URL)
.accept(MediaType.APPLICATION_JSON_VALUE)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(json.getBytes()))
.andExpect(status().isOk())
.andDo(print());
}
@Test
@Transactional
@Rollback()
public void deleteById() throws Exception {
mockMvc.perform(MockMvcRequestBuilders
.delete(BASE_URL+"/{id}", "1dd85201-3d91-4b8d-94d1-42e8d33a71b4")
.accept(MediaType.APPLICATION_JSON_VALUE))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
}
3.2 service实现类 单元测试
package com.example.demo.service.impl;
import com.example.demo.entity.Product;
import com.example.demo.mapper.ProductMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
/**
* @Author huangqianqian
* @Date 2021/10/13
* @Desc service 单元测试
* 所有数据均未mock,不依赖上下文
*/
@ExtendWith(MockitoExtension.class)
class ProductServiceImplTest {
@Mock
private ProductMapper productMapper;
private List<Product> getProductList() {
List<Product> mockList = new ArrayList<Product>();
Product mockProduct = getProduct();
Product mockProduct2 = new Product();
mockProduct2.setId("222");
mockProduct2.setName("testTwo");
mockProduct2.setPrice(100.0);
mockList.add(mockProduct);
mockList.add(mockProduct2);
return mockList;
}
private Product getProduct() {
Product mockProduct = new Product();
mockProduct.setId("111");
mockProduct.setName("testOne");
mockProduct.setPrice(100.0);
return mockProduct;
}
@Test
void queryAll() {
// mock值
Product product = new Product();
List<Product> mockProductList = getProductList();
// mock接口请求返回值
Mockito.when(productMapper.selectList(product)).thenReturn(mockProductList);
// 调用接口,返回值采用上方的mock请求
List<Product> productList = productMapper.selectList(product);
// 断言数据是否符合预期
Assertions.assertEquals(mockProductList, productList);
}
@Test
void queryById() {
String id = "111";
Product mockProduct = getProduct();
Mockito.when(productMapper.selectById(id)).thenReturn(mockProduct);
Product result = productMapper.selectById(id);
Assertions.assertEquals(mockProduct, result);
}
@Test
void deleteById() {
String id = "111";
Product product = new Product();
product.setStatus(0);
product.setId(id);
Mockito.when(productMapper.updateById(product)).thenReturn(1);
int result = productMapper.updateById(product);
Assertions.assertEquals(1, result);
}
@Test
void insert() {
Product mockProduct = getProduct();
mockProduct.setId(UUID.randomUUID().toString());
mockProduct.setStatus(1);
Mockito.when(productMapper.insert(mockProduct)).thenReturn(1);
int result = productMapper.insert(mockProduct);
Assertions.assertEquals(1, result);
}
@Test
void update() {
Product mockProduct = getProduct();
Mockito.when(productMapper.updateById(mockProduct)).thenReturn(1);
int result = productMapper.updateById(mockProduct);
Assertions.assertEquals(1, result);
}
}
3.3 service实现类 集成测试
package com.example.demo.service.impl;
import com.example.demo.entity.Product;
import com.example.demo.service.ProductService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.List;
/**
* @Author huangqianqian
* @Date 2021/10/13
* @Desc service 集成测试
* 非mock, 依赖上下文
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ProductServiceImplIntegrationTest {
@Autowired
private ProductService productService;
private static final String id = "1dd85201-3d91-4b8d-94d1-42e8d33a71b4";
private Product getProduct() {
Product mockProduct = new Product();
mockProduct.setId("1dd85201-3d91-4b8d-94d1-42e8d33a71b4");
mockProduct.setName("testOne");
mockProduct.setPrice(100.0);
mockProduct.setStatus(1);
return mockProduct;
}
@Test
void queryAll() {
List<Product> result = productService.queryAll(new Product());
Assertions.assertNotNull(result);
}
@Test
void queryById() {
Product product = productService.queryById(id);
Assertions.assertNotNull(product);
}
@Test
void deleteById() {
boolean result = productService.deleteById(id);
Assertions.assertTrue(result);
}
@Test
void insert() {
Product product = getProduct();
Product result = productService.insert(product);
Assertions.assertNotNull(result);
Assertions.assertEquals(product.getName(), result.getName());
}
@Test
void update() {
Product product = getProduct();
boolean result = productService.update(product);
Assertions.assertTrue(result);
}
}
4 @SpringBootTest
@SpringBootTest注解使用后,会存在上下文切换,启动spring,如果是单元测试的话,使用mock数据,不依赖于上下文,可以不用加这个注解。
5 @AutoConfigureMockMvc,MockMvc
该注解配合MockMvc使用,可以模拟请求。
5.1 基本流程图
5.2 语法(简单写一下):
mockMvc.perform(MockMvcRequestBuilders.请求方式(“url”).contentType(MediaType.APPLICATION_FORM_URLENCODED).param(“键”,“值”);
5.3 MockMvc说明
- 服务器端SpringMVC测试的主入口点。
- 通过MockMVCBuilders建造者的静态方法去建造MockMVCBuilder,MockMvc由MockMVCBuilder构造。
- 核心方法:perform(RequestBuilder rb),执行一个RequestBuilder请,会自动执行SpringMVC的流程并映射到相应的控制器执行处理,该方法的返回值是一个ResultActions。
5.4 基本组成与流程
MockMvc的几个重要组件如下:
5.4.1 MockMVCBuilder
- MockMVCBuilder是使用构造者模式来构造MockMvc的构造器。
- 主要有两个实现:StandaloneMockMvcBuilder和DefaultMockMvcBuilder。
- 可以直接使用静态工厂MockMvcBuilders创建即可,不需要直接使用上面两个实现类。
5.4.2 MockMVCBuilders
- 负责创建MockMVCBuilder对象。
- 有两种创建方式
- standaloneSetup(Object... controllers): 通过参数指定一组控制器,这样就不需要从上下文获取了。
- webAppContextSetup(WebApplicationContext wac):指定WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的MockMvc
5.4.3 MockMvcRequestBuilders
- 创建请求,可以设置参数、头信息、编码、Cookies等基本http请求所含的所有信息。
- 其主要有两个子类MockHttpServletRequestBuilder和MockMultipartHttpServletRequestBuilder(如文件上传使用),即用来Mock客户端请求需要的所有数据。
5.4.4 MockMvc
客户端,主要入口,执行请求。
5.4.5 ResultActions
结果与动作,MockMvc将MockMvcRequestBuilders构造的请求发出后,返回的结果。并且可以在此基础上,针对结果添加一些动作。包含:
- andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确。
- andDo:添加ResultHandler结果处理器,比如调试时打印结果到控制台。常用的为print(),打印结果
- andReturn:最后返回相应的MvcResult;然后进行自定义验证/进行下一步的异步处理。其中andExpect与andDo返回的类型扔为ResultActions,故可以使用链式的方式添加多个动作。
- MockMvcResultMatchers
- 用来匹配执行完请求后的**结果验证。
- 结果匹配失败将抛出相应的异常。
- 包含了很多验证API方法。
- MockMvcResultHandlers
- 结果处理器,表示要对结果做点什么事情。
- 比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。
5.5 JsonPath
JsonPath是一个用于读取JSON文档的Java DSL。,
MockMvc引入它用于andExpect中来读取、比对json结果。通过表达式读取文档,并传入比对的方法以此进行判断。
写法可以是
$.store.book[0].title
或
$['store']['book'][0]['title']
JsonPath的表达式的语法类似于jquery:
操作符 | 描述 |
$ | 要查询的根元素。所有表达式开始元素。 |
@ | 根据过滤表达式查询节点 |
* | 通配符。可在任何名称或数字需要的地方使用。 |
.. | 深层扫描。可在任何需要名称的地方使用。 |
.<name> | 获取节点 |
['<name>' (, '<name>')] | 根据名称获取子节点$['store']['book'] |
[<number> (, <number>)] | 根据索引获取子节点$['store'][0] |
[start:end] | 获取从start到end的节点列表 |
[?(<expression>)] | 过滤表达式。表达式必须求值为布尔值。 |
6 测试报告输出
参考之前写的文章,或者看pom文件,里面有标注执行命令
(19条消息) maven junit surefire cobertura 测试报告_墨渊er的博客-CSDN博客
7 参考文献
mockMvc使用教程 - 码农教程 (manongjc.com)