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