SpringTest 框架 MVC 场景初体验
SpringTest 框架 MVC 场景初体验
官方资料
官方文档;[官方代码实例](spring-framework/spring-test/src/test/java/org/springframework/test/web/servlet/samples at main · spring-projects/spring-framework · GitHub);
这篇博客基本上相当于官方文档的中文翻译,同时还增加了自己的思考,值得跟官方文档对照着学习:https://segmentfault.com/a/1190000024443851
以后有需要有时间再来仔细地看这些资料,
服务器端测试
这是我们主要的使用场景,直接先看代码,再梳理一下 API
示例代码
引入 pom 依赖
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>${junit.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>${junit.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>${spring.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
</dependency>
被测试的控制器
@Controller
@RequestMapping("/testParams")
public class TestParamsController {
    @RequestMapping("/testSpringMVCAPI")
    public String testSpringMVCAPI(String username, String password) {
        System.out.println(username + password);
        return "target";
    }
    // 注意:此时Form表单的post请求无效,会报格式不支持,因为form表单提交的请求的请求体格式是application/x-www-form-urlencoded,HttpMessageConverter不知道hi,
    // 需要发起消息体格式为application/json的post请求
    @RequestMapping("/testSpringMVCAPIRequestBody")
    public String testSpringMVCAPIPOJOMapping(@RequestBody User body2user, User user) {
        System.out.println(body2user.toString());
        System.out.println(user.toString());
        return "target";
    }
}
MVCMock 测试代码
package xyz.xiashuo.springmvchttpparams.controllertest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
@SpringJUnitWebConfig(locations = "classpath:springMVC.xml")
public class SpringMVCAPITest {
    MockMvc mockMvc;
    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }
    @Test
    void testSpringMVCAPI() throws Exception {
        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        map.add("username", "xiashuo");
        map.add("password", "7879787");
        this.mockMvc.perform(post("/testParams/testSpringMVCAPI")
                        .queryParams(map))
                .andExpect(status().isOk());
        // 模拟表单发起的请求
        //this.mockMvc.perform(post("/testParams/testSpringMVCAPI")
        //                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
        //                .content("username=xiashuo&password=7879787")
        //                )
        //        .andExpect(status().isOk());
        // 模拟ajax发起的json请求,请求进入控制器方法,但是控制器方法无法获取json中的参数
        //this.mockMvc.perform(post("/testParams/testSpringMVCAPI")
        //                .contentType(MediaType.APPLICATION_JSON)
        //                .content("{\"username\":\"xiashuo\",\"password\":7879787}")
        //        )
        //        .andExpect(status().isOk());
    }
    @Test
    void testSpringMVCAPIRequestBody() throws Exception {
        // json格式的消息体,可以到达控制器方法
        this.mockMvc.perform(
                        post("/testParams/testSpringMVCAPIRequestBody")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{ \"username\": \"sd\", \"gender\": \"男\", \"age\": 12 }\n")
                )
                .andExpect(status().isOk())
                .andExpect(view().name("target"));
        // form格式的消息体,无法到达控制器方法,报错不支持的格式,415
        //MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        //map.add("username", "xiashuo");
        //map.add("password", "123456798");
        //map.add("gender", "男");
        //this.mockMvc.perform(
        //                post("/testParams/testSpringMVCAPIRequestBody")
        //                        .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
        //                        .content("")
        //        )
        //        .andExpect(status().isUnsupportedMediaType());
    }
}
API 梳理
我们直接来梳理一下示例代码中使用的主要的 API。
发起请求
发起模拟的 http 请求的方式是 MockMvc.perform(),需要传入 RequestBuilder 类型的参数,MockMvcRequestBuilders 中提供了 HTTP 请求方法对应的返回 RequestBuilder 的方法,比如常用的 put()、delete()、post()、get(),参数为请求路径,基本上所有 HTTP 请求方法都有对应的 Java 方法与之对应,其中最常用的还是 post() 和 get(),
返回的 RequestBuilder 中最常见的实现类型有两个 MockHttpServletRequestBuilder 和 MockMultipartHttpServletRequestBuilder
- 
MockHttpServletRequestBuilder,提供了共各种方法来自定义 http 请求的各个方面,是否使用 HTTPS(secure() 方法),消息头 (header() 或者 headers() 方法),消息体 (content() 方法),此外一些常用的消息头还有单独有方法设置,比如 Content-Type(contentType() 方法),比如 Accept(accept() 方法),还可以设置 cookie(cookie() 方法)、session(session() 方法)、session 属性(sessionAttr() 方法)、request 属性(requestAttr() 方法)。 还有一些不那么直观但是好用的方法,比如 param 方法或者 params 方法或者 queryParam 方法或者 queryParams 方法,先说 param 方法(params 方法与之类似):在 Servlet API 中,请求参数可以从查询字符串(query string,指的是 URL 后的查询字符串)或者 application/x-www-form-urlencoded格式的请求的消息体中解析。param 方法直接将参数添加添加到请求参数映射结果中(SpringMVC 会对调用 servletAPI 对 HTTP 请求的各种参数进行预处理,放到请求参数映射结果中,param 方法相当于跳过这一步,直接将结果放到处理结果中)。你也可以使用添加 Servlet 请求参数,通过以下方式指定 URL 查询参数或表单数据,通过调用 queryParam 方法可以将参数添加到 URL 的查询字符串中(同时 queryParam 方法也会将结果添加到请求参数映射结果中),通过调用 content 方法写入请求消息体的内容,同时通过 contentType 方法指定消息体的格式。@Test void testGetUser() throws Exception { this.mockMvc.perform(get("/getUser") .param("name","xiashuo") .param("gender","男") ) .andDo(print()); }使用 param 方法或者 params 方法或者 queryParam 方法或者 queryParams 方法,都可以直接将参数添加到请求参数映射结果中,这样,参数始终都能推送到控制器方法并注入到参数中,这其实减少了我们测试控制器的工作量,因为有的控制器的参数对参数的来源位置有要求,比如@RequestBody,当然你也可以进行更精细的控制,指定消息体的内容格式,contentType 方法,写入消息体内容 content。 @Test void test() throws Exception { mockMvc.perform( post("/testURL") // 最准确的测试肯定是指定请求体类型和请求体内容 //.contentType(MediaType.APPLICATION_FORM_URLENCODED) //.content("_method=delete&nameParam=xiashuo") // 方便起见,还是直接使用 parma参数吧 .param("_method","delete") .param("nameParam","xiashuo") ) .andDo(print()); }直接将 JavaBean 写入字节数组到 content 方法中,首先 JavaBean 所在的类需要实现序列化接口 Serializable,以 User 类为例@Component @Data public class User implements Serializable { private static final long serialVersionUID = 2241436260958561580L; @NotNull @NotBlank private String name; @NotNull @NotBlank private String gender; @Email private String email; @PastOrPresent private Date birthDate; @Min(1) @Max(100) private Integer age; }使用 org.springframework.util.SerializationUtils来序列化对象User user = new User(); user.setName("xiashuo"); user.setAge(18); user.setGender("男"); user.setEmail("[email protected]"); user.setBirthDate(new Date()); byte[] userData = SerializationUtils.serialize(user);然后将其放入 content() 方法即可。 对于 json 类型的方法体,也可以直接采用第三方的库来直接字符串话 JavaBean @Test void addUser() throws Exception { User user = new User(); user.setName("xiashuo"); user.setAge(18); user.setGender("男"); user.setEmail("[email protected]"); user.setBirthDate(new Date()); //byte[] userData = SerializationUtils.serialize(user); byte[] userData = JSON.toJSONString(user).getBytes(StandardCharsets.UTF_8); mockMvc.perform(post("/module/user").contentType(MediaType.APPLICATION_JSON).content(userData)).andExpect(view().name("result")).andDo(print()); }
- 
MockMultipartHttpServletRequestBuilder 继承 MockHttpServletRequestBuilder,用于模拟文件上传的请求 @Test void testUpload() throws Exception { File file = ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + File.separator + "sourceFile" + File.separator + "outline.png"); byte[] fileContent = FileUtils.readFileToByteArray(file); MockMultipartFile multipartFile = new MockMultipartFile("file", "outline.png", MediaType.IMAGE_PNG_VALUE, fileContent); mockMvc.perform(MockMvcRequestBuilders.multipart("/file/upload").file(multipartFile)).andDo(print()); }
验证请求结果
发送了请求,我们就得验证这个请求返回的结果是不是我们期望的结果,MockMvc.perform() 方法返回的类型是 ResultActions,我们调用其 andExpect 方法进行判断,andExpect 方法参数为 ResultMatcher,MockMvcResultMatchers 中提供了各种 ResultMatcher 模板,通过传入参数即可返回特定的 ResultMatcher 对象,我们列举几个常用的:
- 
MockMvcResultMatchers.model() 返回的 ModelResultMatchers,可以对请求的 Model 中的属性值进行验证。比如 attribute() 方法。 
- 
MockMvcResultMatchers.status() 返回 StatusResultMatchers,可以对响应的状态进行验证,常用的有 isOk() 表示正常,响应状态码为 200,isUnsupportedMediaType() 不支持的媒体类型,响应状态码为 415,各种响应匹配,应有尽有。 
- 
MockMvcResultMatchers.content() 返回 ContentResultMatchers,可以对响应的消息体进行验证,常用的方法有响应体格式 contentType(),响应体编码 encoding() 
- 
MockMvcResultMatchers.view() 返回的是 ViewResultMatchers,可以对控制器返回的视图进行验证,常用的方法就是 name(),看返回的视图的名称是不是指定的视图的名称。 
MockMvcResultMatchers 中还有其他的 ResultMatcher 类,应有尽有,绝对够用了。
大部分的时候,我们都想在模拟请求执行完之后做一点事情,比如打印日志,看看模拟请求的具体情况,这个时候,可以调用 ResultActions 的 andDo 方法,andDo 方法的参数是 ResultHandler,MockMvcResultHandlers 中提供了各种 ResultHandler 模板,比如 print 方法(在控制台输出模拟请求的详细信息)和 log 方法(记录模拟请求的信息到测试日志中)
this.mockMvc.perform(
                post("/testParams/testSpringMVCAPIRequestBody")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{ \"username\": \"sd\", \"gender\": \"男\", \"age\": 12 }\n")
        )
        .andExpect(status().isOk())
        .andExpect(view().name("target")).andDo(print());
输出日志,可以很清晰的看到模拟请求和模拟响应,模拟响应是一个 HTML 页面。
 
在某些情况下,你可能希望直接访问请求的结果并验证一些 ResultMatcher 无法验证的内容,或者你需要拿这个请求返回结果进行进一步的操作,比如 asyncDispatch 异步请求。可以调用 ResultActions 的 andReturn 方法,直接在所有 andExpect 和 andDo 之后添加 .andReturn() 来实现,如以下示例所示:
MvcResult target = this.mockMvc.perform(
                post("/testParams/testSpringMVCAPIRequestBody")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{ \"username\": \"sd\", \"gender\": \"男\", \"age\": 12 }\n")
        )
        .andExpect(status().isOk())
        .andExpect(view().name("target")).andReturn();
// 这里输出的就是target视图解析出来的HTML页面
System.out.println(target.getResponse().getContentAsString());
输出页面
 
当然了,这里只是打个样,如果你真的想要输出视图的 html,那还是通过 andDo(print()) 方便些。
使用前端发起请求 -- 和 HtmlUnit 的集成
HtmlUnit 是一个“面向 Java 程序的无 gui 浏览器”。它对 HTML 文档进行建模,并提供了一个 API,允许您调用页面、填写表单、点击链接等。就像你在“普通”浏览器中做的那样。
意思就是允许你在 Java 代码中模拟 hmtl 页面填写表格的操作。主要用于测试框架,相当于我们可以模拟普通使用场景的手点操作,这样的跟实际使用一模一样的测试,才是真测试,
这里目前我们用不到,先不学了,TODO
注意
- 
无法使用 web.xml 中的配置,比如过滤器,如果你想要使用过滤器,只能使用 DefaultMockMvcBuilder手动构建。比如添加过滤器addFilter
- 
当响应的消息体的媒体类型为 JSON 类型的时候,是没有 ModelAndView,因为只有返回视图进行视图解析的时候,才会有 ModelAndView