Spring MVC中的Controller进行单元测试的实现
对Controller进行单元测试是Spring框架原生就支持的能力,它可以模拟HTTP客户端发起对服务地址的请求,可以不用借助于诸如Postman这样的外部工具就能完成对接口的测试。
具体来讲,是由Spring框架中的spring-test
模块提供的实现,详见MockMvc。
如下将详细阐述如何使用MockMvc测试框架实现对“Spring Controller”进行单元测试,基于Spring Boot开发框架进行验证。
添加测试框架依赖:
<!-- Spring框架 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> !<-- Spring测试框架 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- 文件操作工具 --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency>
导入静态工具方法
为了便于在编写测试用例时直接调用测试框架自带的静态方法,首先需要导入这些静态工具方法。
需要导入的静态方法如下:
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.setup.SharedHttpSessionConfigurer.*;
初始化MockMvc
初始化MockMvc有2种方式:
方式1:明确指定需要测试的“Controller”类进行配置
方式2:基于Spring容器进行配置,包含了Spring MVC环境和所有“Controller”类,通常使用这种方式。
@SpringBootTest public class TestControllerTest { MockMvc mockMvc; // 初始化MockMvc @BeforeEach void setUp(WebApplicationContext wac) { // 方式1:明确指定需要测试的“Controller”类 this.mockMvc = MockMvcBuilders.standaloneSetup(new TestController()).build(); // 方式2:基于Spring容器进行配置,包含了Spring MVC环境和所有“Controller”类。 this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } }
另外,还可以对MockMvc进行全局配置。
// 全局配置MockMvc this.mockMvc = MockMvcBuilders.webAppContextSetup(wac) .defaultRequest(get("/").accept(MediaType.APPLICATION_JSON)) // 默认请求路径 .apply(sharedHttpSession()) // 配置session .alwaysExpect(status().isOk()) // 预期响应状态码 .alwaysExpect(content().contentType("application/json;charset=UTF-8")) // 预期内容类型 .build();
执行测试
MockMvc支持对常见的HTTP方法,如:GET,POST,PUT,DELETE等,甚至还支持文件上传请求。
测试GET接口
// 访问GET接口:不带参数 @Test public void testSimpleGet() throws Exception { MvcResult result = this.mockMvc.perform(get("/test/simple/get") .accept(MediaType.APPLICATION_JSON)) // 接受JSON格式响应消息 .andReturn(); // 获取返回结果 Assertions.assertEquals("OK", result.getResponse().getContentAsString()); } // 访问GET接口:带URL参数 @Test public void testParamGet() throws Exception { int id = 10; // 方式1:在URI模板中指定参数 //MvcResult result = this.mockMvc.perform(get("/test/param/get?id={id}", id).accept(MediaType.APPLICATION_JSON)).andReturn(); // 方式2:通过param()方法指定参数 //MvcResult result = this.mockMvc.perform(get("/test/param/get").param("id", String.valueOf(id)).accept(MediaType.APPLICATION_JSON)).andReturn(); // 方式3:通过queryParam()方法指定参数 MvcResult result = this.mockMvc.perform(get("/test/param/get").queryParam("id", String.valueOf(id)).accept(MediaType.APPLICATION_JSON)).andReturn(); Assertions.assertEquals("OK: " + id, result.getResponse().getContentAsString()); }
测试POST接口
// 传递表单参数 @Test public void testSimplePost() throws Exception { int id = 10; // 调用param()方法传递参数 MvcResult result = this.mockMvc.perform(post("/test/simple/post") .param("id", String.valueOf(id)) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .accept(MediaType.APPLICATION_JSON)) .andReturn(); Assertions.assertEquals("{\"id\":10}", result.getResponse().getContentAsString()); } // 传递JSON参数 @Test public void testSimplePostJson() throws Exception { // 调用content()方法传递json字符串参数 Subject subject = new Subject(); subject.setId(10); String content = JSON.toJSONString(subject); MvcResult result = this.mockMvc.perform(post("/test/simple/post/json") .content(content) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)) .andReturn(); Assertions.assertEquals("{\"id\":10}", result.getResponse().getContentAsString()); }
测试文件上传
@Test public void testFileUploadSingle() throws Exception { File file = new File("C:\\Users\\xxx\\Downloads\\test.jpg"); String fileName = FilenameUtils.getName(file.getName()); byte[] bytes = FileUtils.readFileToByteArray(file); MockMultipartFile mockMultipartFile = new MockMultipartFile("file", fileName, MediaType.MULTIPART_FORM_DATA_VALUE, bytes); this.mockMvc.perform(multipart("/test/upload/single").file(mockMultipartFile)) .andExpect(status().isOk()) .andExpect(content().string("OK")) .andDo(print()); }
定义预期结果
断言响应结果时,有2种方式:
1.使用JUnit提供的Assert断言工具判断返回结果,这是一种非常普遍和常见的方式
2.在MockMvc框架中可以通过andExpect()
方法定义一个或多个预期结果,当其中一个期望结果断言失败时,就不会断言其他期望值了
// 使用Junit断言工具判断返回结果是否符合预期 @Test public void testAssertResult() throws Exception { MvcResult result = this.mockMvc.perform(get("/test/simple/get").accept(MediaType.APPLICATION_JSON)).andDo(print()).andReturn(); Assert.assertEquals("OK", result.getResponse().getContentAsString()); } // 在MockMvc框架中定义预期结果 @Test public void testExpectations() throws Exception { this.mockMvc.perform(get("/test/simple/get").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // 预期响应状态码为200 .andExpect(content().string("OK")) // 预期返回值为字符串“OK” .andDo(print()); }
相比于使用Junit的断言工具判断返回结果,在MockMvc框架中直接定义预期结果进行断言检查更加简洁。
写在最后
使用Spring提供的测试框架MockMvc可以非常方便地实现对HTTP服务接口进行单元测试,不要把基础的功能验证工作都交给测试童鞋,应该通过单元测试来保证代码迭代的稳定性。