单元测试实践三板斧-Triple Kill

一、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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值