一、Controller层单元测试
说起Web Controller层的单元测试,可能许多研发同学都会觉得这层测试可有可无,因为我们这里没有复杂的业务逻辑。
首先可以肯定的是Controller层我们确实没有也不应该有过于复杂的业务逻辑,但是其实仔细梳理,这层也承担着非常多的职责,通过单元测试可以大大减少出错几率,提高开发效率。
从接收请求到数据返回Spring有很多魔法,做了很多事情,总结如下几个步骤:
职责 | 描述 | |
---|---|---|
1. | 监听HTTP请求 | controller需要对特定的URL,HTTP方法和content类型做响应 |
2. | 反序列化输入 | controller需要解析进入的HTTP请求并从URL,HTTP请求参数和请求body中创建Java对象,这样我们在代码中使用 |
3. | 检查输入 | controller是防御不合法输入的第一道防线,所以这是个校验输入的好地方 |
4. | 调用业务逻辑 | 得到了解析过的入参,controller需要将入参传给业务逻辑期望的业务模型 |
5. | 序列化输出 | controller得到业务逻辑的输出并将其序列化到HTTP响应中 |
6. | 翻译异常 | 如果某些地方有异常发生了,controller需要将其翻译成一个合理的错误消息和HTTP状态码 |
二、Controller单测写法
和数据层测试有点类似,在Controller层我们也不能简单的、单纯的去像Service层那样Mock数据完成测试。简单的Mock掉Service层进行的单元测试意义不大,因为我们上面提到的职责单元测试不可以覆盖:
职责 | 描述 | |
---|---|---|
1. | 监听HTTP请求 | 不行,因为单元测试不会检查@PostMapping声明并模拟HTTP请求的特定参数 |
2. | 反序列化输入 | 不行,因为像@RequestParam和@pathVariable这样的声明不会被检验。我们会以Java对象的形式提供输入,这会跳过HTTP请求的反序列化 |
3. | 检查输入 | 不行,不依赖bean校验,因为@Valid声明不会被校验。 |
4. | 调用业务逻辑 | 可以,因为我们可以校验业务逻辑被期望的参数调用 |
5. | 序列化输出 | 不行,因为只能校验Java版本的输出,HTTP返回不会生成 |
6. | 翻译异常 | 不行,我们可以检查一个特定的异常是否产生,但它不会被翻译成一个JSON返回或HTTP状态码 |
从以上表格可以看出,简单的单元测试确实意义不大,所以我们要把Spring引入到测试进来,使用@WebMvcTest来做有一定集成度的测试。
Spock框架 + @WebMvcTest
@WebMvcTest注解,用来声明只加载需要测试的web controller相关bean的应用上下文。SpringBoot默认会加载应用上下文中的所有controller,这样我们就需要加载或模拟每个controller依赖的所有bean,这会使测试的配置变得异常复杂。为了让单元测试保持独立,我们通过配置value参数指定要测试的controller来缩小上下文范围。
MockMvc,使用MockMvc来模拟Http请求,基于RESTful风格的SpringMVC的测试,我们可以测试完整的Spring MVC流程,即从URL请求到控制器处理,再到视图渲染都可以测试。测试类使用@WebMvcTest注解标记后,MockMvc可以直接通过@Autowire完成注入。
@WebMvcTest(value = CategoryController.class)
class CategoryControllerTest extends Specification {
@Autowired
MockMvc mvc;
@SpringBean
CategoryService categoryService = Mock();
def "getCategoryByLevel"() {
given: "数据准备"
categoryService.getValidCategoryByLevel(_) >> [
new Category(level: 1, id: 1, name: "数码产品"),
new Category(level: 1, id: 2, name: "视频百货")
]
when: "执行测试"
def res = mvc.perform(get("/category/getCategoryByLevel")
.param("level", "1")
.accept("application/json;charset=UTF-8"))
.andDo(print())
.andReturn()
then: "验证结果"
verifyAll {
res.response.status == HttpServletResponse.SC_OK
String data = res.response.contentAsString;
JSON.parseObject(data).get("success") == true;
JSON.parseObject(data).getJSONArray("data").size() == 2;
}
}
}
Junit框架 + @WebMvcTest
@WebMvcTest(value = CategoryController.class)
public class CategoryControllerTest {
@Autowired
MockMvc mvc;
@MockBean
private CategoryService categoryService;
@Test
public void getCategoryByLevel() throws Exception {
List<Category> categories = new ArrayList<>();
Category category1 = new Category();
category1.setLevel(1);
category1.setId(1);
category1.setName("数码产品");
Category category2 = new Category();
category2.setLevel(1);
category2.setId(2);
category2.setName("食品百货");
categories.add(category1);
categories.add(category2);
Mockito.when(categoryService.getValidCategoryByLevel(ArgumentMatchers.any()))
.thenReturn(categories);
mvc.perform(get("/category/getCategoryByLevel")
.param("level", "1")
.accept("application/json;charset=UTF-8")
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(2));
}
}
MockMvc使用
构建Request请求,验证Http请求URL是否匹配,http方法、content type是否正确,如果不匹配测试会失败。校验入参是否可以成功的被接收并转化为Java对象,校验返回结果是否符合预期
mockMvc.perform(post("/diagnosis/rule/{ruleId}", 111L)
.contentType("application/json")
.param("sendMail", "true")
.content(JSON.toJSONString(ruleVO)))
.andExpect(status().isOk());
如上代码,通过POST方式请求“/diagnosis/rule/{ruleId}”路径,请求中提供了路径变量(@PathVariable)ruleId,方法级别参数(@RequestParam)sendMail,请求body内容(@RequestBody);通过andExpect()去验证结果是否符合预期。
常用验证项
// 使用jsonPaht验证返回的json中msg、success字段的返回值
.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("成功"))
.andExpect(MockMvcResultMatchers.jsonPath("$.success").value(true))
// 数据属性不为空
.andExpect(MockMvcResultMatchers.jsonPath("$.data").isNotEmpty())
// 期望的返回结果集合有2个元素,$:返回结果
.andExpect(MockMvcResultMatchers.jsonPath("$.data.length()").value(2));
常用API解释
/** RequestBuilder/MockMvcRequestBuilders:*/
//根据uri模板和uri变量值得到一个GET请求方式的MockHttpServletRequestBuilder;
MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables)
//同get类似,但是是POST方法;
MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables)
//同get类似,但是是PUT方法;
MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables)
//同get类似,但是是DELETE方法;
MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables)
//同get类似,但是是OPTIONS方法;
MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables)
//提供自己的Http请求方法及uri模板和uri变量,如上API都是委托给这个API;
MockHttpServletRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables)
//:添加头信息;
MockHttpServletRequestBuilder header(String name, Object... values)/MockHttpServletRequestBuilder headers(HttpHeaders httpHeaders)
//:指定请求的contentType头信息;
MockHttpServletRequestBuilder contentType(MediaType mediaType)
//:指定请求的Accept头信息;
MockHttpServletRequestBuilder accept(MediaType... mediaTypes)
//:指定请求Body体内容;
MockHttpServletRequestBuilder content(String content)
//:请求传入参数
MockHttpServletRequestBuilder param(String name,String... values)
//:指定请求的Cookie;
MockHttpServletRequestBuilder cookie(Cookie... cookies)
//:指定请求字符编码;
MockHttpServletRequestBuilder characterEncoding(String encoding)
//:指定要上传的文件;
MockMultipartHttpServletRequestBuilder file(String name, byte[] content)/MockMultipartHttpServletRequestBuilder file(MockMultipartFile file)
//:添加验证断言来判断执行请求后的结果是否是预期的;
ResultActions andExpect(ResultMatcher matcher)
//:添加结果处理器,用于对验证成功后执行的动作,如输出下请求/结果信息用于调试;
ResultActions andDo(ResultHandler handler)
//:返回验证成功后的MvcResult;用于自定义验证/下一步的异步处理;
MvcResult andReturn()
//:请求的Handler验证器,比如验证处理器类型/方法名;此处的Handler其实就是处理请求的控制器;
HandlerResultMatchers handler()
//:得到RequestResultMatchers验证器;
RequestResultMatchers request()
//:得到模型验证器;
ModelResultMatchers model()
//:得到视图验证器;
ViewResultMatchers view()
//:得到响应状态验证器;
StatusResultMatchers status()
//:得到响应Header验证器;
HeaderResultMatchers header()
//:得到响应Cookie验证器;
CookieResultMatchers cookie()
//:得到响应内容验证器;
ContentResultMatchers content()
//:得到Json表达式验证器;
JsonPathResultMatchers jsonPath(String expression, Object ... args)/ResultMatcher jsonPath(String expression, Matcher matcher)
//:得到Xpath表达式验证器;
XpathResultMatchers xpath(String expression, Object... args)/XpathResultMatchers xpath(String expression, Map<string, string=""> namespaces, Object... args)
//:验证处理完请求后转发的url(绝对匹配);
ResultMatcher forwardedUrl(final String expectedUrl)
//:验证处理完请求后转发的url(Ant风格模式匹配,@since spring4);
ResultMatcher forwardedUrlPattern(final String urlPattern)
//:验证处理完请求后重定向的url(绝对匹配);
ResultMatcher redirectedUrl(final String expectedUrl)
//:验证处理完请求后重定向的url(Ant风格模式匹配,@since spring4);
ResultMatcher redirectedUrlPattern(final String expectedUrl)
参考资料:
https://reflectoring.io/spring-boot-web-controller-test/
https://blog.csdn.net/darkjune/article/details/114256161