今天来学习下如何使用Spring Mvc来对controller定义的Restful API进行集成测试。MockMVC 类是Spring test 框架的一部分,因此不需要额外引入单独的Maven依赖。使用Spring MockMvc有以下优点
首先,在pom文件中添加以下依赖
org.springframework.boot spring-boot-starter-test test
org.mockito mockito-core 4.8.1
为了熟悉Mockio的各种API,先自定义一个基础的类,在这个类的基础上实现各种mock操作。
import java.util.AbstractList;
public class MyList extends AbstractList {@Overridepublic String get(int index) {//注意 get方法默认返回给 null,方便后续mockreturn null;}@Overridepublic 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 {// 各种测试方法
}
正常情况下,调用list 接口的add方法往列表中添加元素,返回true表示添加成功,否则反之。现在测试阶段可以通过mock方式控制其返回值(当然这并没有任何实际意义,仅仅只是为了属性相关的API)
@Testvoid test(){//对MyList对象进行mockMyList listMock = Mockito.mock(MyList.class);//对象add 任何数据时 返回falsewhen(listMock.add(anyString())).thenReturn(false);boolean added = listMock.add("hello");//通过断言判断返回值assertThat(added).isFalse();}
以上的方法非常的简单易懂,核心代码也很好理解,不做过多解释。此外还有另外一种API能够实现相同的功能,从语法的角度来讲,区别仅仅只是将目的状语前置
@Testvoid test2(){//对MyList对象进行mockMyList listMock = Mockito.mock(MyList.class);//返回false 当对象添加任意string元素时doReturn(false).when(listMock).add(anyString());boolean added = listMock.add("hello");assertThat(added).isFalse();}
当程序内部发生异常时,来看看mock是如何处理的。
@Testvoid test3ThrowException(){MyList mock = Mockito.mock(MyList.class);//添加数据时 抛出异常when(mock.add(anyString())).thenThrow(IllegalStateException.class);assertThatThrownBy(() -> mock.add("hello")).isInstanceOf(IllegalStateException.class);}
以上的代码仅仅只是对异常的类型对了判断。如果还需要对异常报错信息进行判断比对的话,请看下面的代码
@Testvoid 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");}
在需要的时候,mockio框架提供相关API,让特定的方法做真实的调用(调用真实方法逻辑)
@Testvoid testRealCall(){MyList mock = Mockito.mock(MyList.class);when(mock.size()).thenCallRealMethod();assertThat(mock).hasSize(2);}
这里的放回跟整体方法的返回在概念上并不一致。当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");
}
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> 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 removeEmployee (@PathVariable("id") int id) {return new ResponseEntity(HttpStatus.ACCEPTED);}@PostMapping(value = "/employees")public ResponseEntity addEmployee (@RequestBody EmployeeVO employee){return new ResponseEntity(employee, HttpStatus.CREATED);}@PutMapping(value = "/employees/{id}")public ResponseEntity updateEmployee (@PathVariable("id") int id,@RequestBody EmployeeVO employee){return new ResponseEntity(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 {@Autowiredprivate MockMvc mvc;}
@Testvoid 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)));}
@Testvoid 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
@Testvoid 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("东方不败"));}
@Testvoid removeEmployee() throws Exception {mvc.perform( MockMvcRequestBuilders.delete("/employees/{id}", 1) ).andExpect(status().isAccepted());}