一、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()去验证结果是否符合预期。
常用验证项
.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("成功"))
.andExpect(MockMvcResultMatchers.jsonPath("$.success").value(true))
.andExpect(MockMvcResultMatchers.jsonPath("$.data").isNotEmpty())
.andExpect(MockMvcResultMatchers.jsonPath("$.data.length()").value(2));
常用API解释
MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables)
MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables)
MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables)
MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables)
MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables)
MockHttpServletRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables)
MockHttpServletRequestBuilder header(String name, Object... values)/MockHttpServletRequestBuilder headers(HttpHeaders httpHeaders)
MockHttpServletRequestBuilder contentType(MediaType mediaType)
MockHttpServletRequestBuilder accept(MediaType... mediaTypes)
MockHttpServletRequestBuilder content(String content)
MockHttpServletRequestBuilder param(String name,String... values)
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 andReturn()
HandlerResultMatchers handler()
RequestResultMatchers request()
ModelResultMatchers model()
ViewResultMatchers view()
StatusResultMatchers status()
HeaderResultMatchers header()
CookieResultMatchers cookie()
ContentResultMatchers content()
JsonPathResultMatchers jsonPath(String expression, Object ... args)/ResultMatcher jsonPath(String expression, Matcher matcher)
XpathResultMatchers xpath(String expression, Object... args)/XpathResultMatchers xpath(String expression, Map<string, string=""> namespaces, Object... args)
ResultMatcher forwardedUrl(final String expectedUrl)
ResultMatcher forwardedUrlPattern(final String urlPattern)
ResultMatcher redirectedUrl(final String expectedUrl)
ResultMatcher redirectedUrlPattern(final String expectedUrl)
参考资料: https://reflectoring.io/spring-boot-web-controller-test/ https://blog.csdn.net/darkjune/article/details/114256161
|