Spring MockMvc
今天来学习下如何使用Spring Mvc来对controller定义的Restful API进行集成测试。MockMVC 类是Spring test 框架的一部分,因此不需要额外引入单独的Maven依赖。使用Spring MockMvc有以下优点
- 使开发人员摆脱第三方工具的依赖,如Postman、Apipost等
- 微服务架构,团队之间的配合协调并不一致。如下单流程测试,需要订单微服务提供接口做全流程测试,但是订单接口尚未准备好,这时可以使用Mock功能进行模拟测试
Maven依赖
首先,在pom文件中添加以下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.8.1</version>
</dependency>
Mockito 基本使用
基础代码
为了熟悉Mockio的各种API,先自定义一个基础的类,在这个类的基础上实现各种mock操作。
import java.util.AbstractList;
public class MyList extends AbstractList<String> {
@Override
public String get(int index) {
//注意 get方法默认返回给 null,方便后续mock
return null;
}
@Override
public int size() {
return 1;
}
}
接着定义一个测试的基础骨架类,后续针对每一类测试场景,在类中添加方法即可
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
public class MyListMock {
// 各种测试方法
}
Mock add方法
正常情况下,调用list 接口的add方法往列表中添加元素,返回true表示添加成功,否则反之。现在测试阶段可以通过mock方式控制其返回值(当然这并没有任何实际意义,仅仅只是为了属性相关的API)
@Test
void test(){
//对MyList对象进行mock
MyList listMock = Mockito.mock(MyList.class);
//对象add 任何数据时 返回false
when(listMock.add(anyString())).thenReturn(false);
boolean added = listMock.add("hello");
//通过断言判断返回值
assertThat(added).isFalse();
}
以上的方法非常的简单易懂,核心代码也很好理解,不做过多解释。此外还有另外一种API能够实现相同的功能,从语法的角度来讲,区别仅仅只是将目的状语前置
@Test
void test2(){
//对MyList对象进行mock
MyList listMock = Mockito.mock(MyList.class);
//返回false 当对象添加任意string元素时
doReturn(false).when(listMock).add(anyString());
boolean added = listMock.add("hello");
assertThat(added).isFalse();
}
Mock 异常处理
当程序内部发生异常时,来看看mock是如何处理的。
@Test
void test3ThrowException(){
MyList mock = Mockito.mock(MyList.class);
//添加数据时 抛出异常
when(mock.add(anyString())).thenThrow(IllegalStateException.class);
assertThatThrownBy(() -> mock.add("hello"))
.isInstanceOf(IllegalStateException.class);
}
以上的代码仅仅只是对异常的类型对了判断。如果还需要对异常报错信息进行判断比对的话,请看下面的代码
@Test
void test4ThrowException(){
MyList mock = Mockito.mock(MyList.class);
//抛出异常 并指定异常信息
doThrow(new IllegalStateException("error message")).when(mock).add(anyString());
assertThatThrownBy(() -> mock.add("hello"))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("error message");
}
Mock 真实调用
在需要的时候,mockio框架提供相关API,让特定的方法做真实的调用(调用真实方法逻辑)
@Test
void testRealCall(){
MyList mock = Mockito.mock(MyList.class);
when(mock.size()).thenCallRealMethod();
assertThat(mock).hasSize(2);
}
Mock 定制返回
这里的放回跟整体方法的返回在概念上并不一致。当List存在多个元素时,Mock框架可以对特定的元素进行mock
@Test
void testCustomReturn(){
MyList mock = Mockito.mock(MyList.class);
mock.add("hello");
mock.add("world");
//修改下标为0的值
doAnswer(t -> "hello world").when(mock).get(0);
String element = mock.get(0);
assertThat(element).isEqualTo("hello world");
}
RestController Mock
Mock 框架同样支持对 Restful 风格的controller层面的代码进行mock,为了更加直观的看到演示效果,先定义一个简单的controller,内部定义了http 不同请求类型的方法。
基础代码
- 基础VO类
import lombok.*;
@Data
@Builder
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class EmployeeVO {
private Long id;
private String name;
}
- RestController
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@RestController
public class MvcController {
@GetMapping(value = "/employees")
public Map<String,List<EmployeeVO>> getAllEmployees(){
return Map.of("data",Arrays.asList(new EmployeeVO(100L,"kobe")));
}
@GetMapping(value = "/employees/{id}")
public EmployeeVO getEmployeeById (@PathVariable("id") long id){
return EmployeeVO.builder().id(id).name("kobe:" + id).build();
}
@DeleteMapping(value = "/employees/{id}")
public ResponseEntity<HttpStatus> removeEmployee (@PathVariable("id") int id) {
return new ResponseEntity<HttpStatus>(HttpStatus.ACCEPTED);
}
@PostMapping(value = "/employees")
public ResponseEntity<EmployeeVO> addEmployee (@RequestBody EmployeeVO employee){
return new ResponseEntity<EmployeeVO>(employee, HttpStatus.CREATED);
}
@PutMapping(value = "/employees/{id}")
public ResponseEntity<EmployeeVO> updateEmployee (@PathVariable("id") int id,@RequestBody EmployeeVO employee){
return new ResponseEntity<EmployeeVO>(employee,HttpStatus.OK);
}
}
controller层定义了不同请求类型HTTP请求。接下来根据不同的请求类型分别进行mock。
为了读者能够更加直观的进行阅读,首先定义Mock测试骨架类,后续不同场景测试代码在该骨架类中添加方法即可
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(MvcController.class)
class MvcControllerTest {
@Autowired
private MockMvc mvc;
}
Mock HTTP GET
@Test
void getAllEmployees() throws Exception{
mvc.perform(MockMvcRequestBuilders
.get("/employees")
//接收header类型
.accept(MediaType.APPLICATION_JSON))
//打印返回
.andDo(print())
// 判断状态
.andExpect(status().isOk())
//取数组第一个值 进行比较
.andExpect(jsonPath("$.data[0].name").value("kobe"))
//取数组第一个值 进行比较
.andExpect(jsonPath("$.data[0].id").value(100L))
//判断返回长度
.andExpect(jsonPath("$.data", hasSize(1)));
}
Mock HTTP POST
@Test
void addEmployee() throws Exception {
mvc.perform( MockMvcRequestBuilders
.post("/employees") // 指定post类型
.content(new ObjectMapper().writeValueAsString(new EmployeeVO(101L,"东方不败")))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated())
//判断返回是否存在id字段
.andExpect(MockMvcResultMatchers.jsonPath("$.id").exists());
}
Mock HTTP PUT
@Test
void updateEmployee() throws Exception {
mvc.perform( MockMvcRequestBuilders
//指定http 请求类型
.put("/employees/{id}", 2)
.content(new ObjectMapper().writeValueAsString(new EmployeeVO(2L,"东方不败")))
//请求header 类型
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(2L))
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("东方不败"));
}
Mock HTTP DELETE
@Test
void removeEmployee() throws Exception {
mvc.perform( MockMvcRequestBuilders.delete("/employees/{id}", 1) )
.andExpect(status().isAccepted());
}