Bootstrap

Spring Boot Mock

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());
    }
;