欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

基于Mock测试Spring MVC接口过程解析

程序员文章站 2022-08-27 12:54:29
1. 前言在java开发中接触的开发者大多数不太注重对接口的测试,结果在联调对接中出现各种问题。也有的使用postman等工具进行测试,虽然在使用上没有什么问题,如果接口增加了权限测试起来就比较恶心了...

1. 前言

在java开发中接触的开发者大多数不太注重对接口的测试,结果在联调对接中出现各种问题。也有的使用postman等工具进行测试,虽然在使用上没有什么问题,如果接口增加了权限测试起来就比较恶心了。所以建议在单元测试中测试接口,保证在交付前先自测接口的健壮性。今天就来分享一下胖哥在开发中是如何对spring mvc接口进行测试的。

在开始前请务必确认添加了spring boot test相关的组件,在最新的版本中应该包含以下依赖:

<dependency>
  <groupid>org.springframework.boot</groupid>
  <artifactid>spring-boot-starter-test</artifactid>
  <scope>test</scope>
  <exclusions>
    <exclusion>
      <groupid>org.junit.vintage</groupid>
      <artifactid>junit-vintage-engine</artifactid>
    </exclusion>
  </exclusions>
</dependency>

本文是在spring boot 2.3.4.release下进行的。

2. 单独测试控制层

如果我们只需要对控制层接口(controller)进行测试,且该接口不依赖@service、@component等注解声明的spring bean时,可以借助@webmvctest来启用只针对web控制层的测试,例如

@webmvctest
class customspringinjectapplicationtests {
  @autowired
  mockmvc mockmvc;

  @sneakythrows
  @test
  void contextloads() {
    mockmvc.perform(mockmvcrequestbuilders.get("/foo/map"))
        .andexpect(resultmatcher.matchall(status().isok(),
            content().contenttype(mediatype.application_json),
            jsonpath("$.test", is.is("hello"))))
        .anddo(mockmvcresulthandlers.print());
  }

}

这种方式要快的多,它只加载了应用程序的一小部分。但是如果你涉及到服务层这种方式是不凑效的,我们就需要另一种方式了。

3. 整体测试

大多数spring boot下的接口测试是整体而又全面的测试,涉及到控制层、服务层、持久层等方方面面,所以需要加载比较完整的spring boot上下文。这时我们可以这样做,声明一个抽象的测试基类:

package cn.felord.custom;

import org.springframework.beans.factory.annotation.autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.autoconfiguremockmvc;
import org.springframework.boot.test.context.springboottest;
import org.springframework.test.web.servlet.mockmvc;


/**
 * 测试基类,
 * @author felord.cn
 */
@springboottest
@autoconfiguremockmvc
abstract class customspringinjectapplicationtests {
  /**
   * the mock mvc.
   */
  @autowired
  mockmvc mockmvc;
  // 其它公共依赖和处理方法 
}

只有当@autoconfiguremockmvc存在时mockmvc才会被注入spring ioc。

然后针对具体的控制层进行如下测试代码的编写:

package cn.felord.custom;

import lombok.sneakythrows;
import org.hamcrest.core.is;
import org.junit.jupiter.api.test;
import org.springframework.http.mediatype;
import org.springframework.test.web.servlet.resultmatcher;
import org.springframework.test.web.servlet.request.mockmvcrequestbuilders;
import org.springframework.test.web.servlet.result.mockmvcresulthandlers;

import static org.springframework.test.web.servlet.result.mockmvcresultmatchers.*;

/**
 * 测试foocontroller.
 *
 * @author felord.cn
 */
public class footests extends customspringinjectapplicationtests {
  /**
   * /foo/map接口测试.
   */
  @sneakythrows
  @test
  void contextloads() {
    mockmvc.perform(mockmvcrequestbuilders.get("/foo/map"))
        .andexpect(resultmatcher.matchall(status().isok(),
            content().contenttype(mediatype.application_json),
            jsonpath("$.test", is.is("bar"))))
        .anddo(mockmvcresulthandlers.print());
  }
}

4. mockmvc测试

集成测试时,希望能够通过输入url对controller进行测试,如果通过启动服务器,建立http client进行测试,这样会使得测试变得很麻烦,比如,启动速度慢,测试验证不方便,依赖网络环境等,为了可以对controller进行测试就引入了mockmvc。

mockmvc实现了对http请求的模拟,能够直接使用网络的形式,转换到controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。接下来我们来一步步构造一个测试的模拟请求,假设我们存在一个下面这样的接口:

@restcontroller
@requestmapping("/foo")
public class foocontroller {
  @autowired
  private mybean mybean;

  @getmapping("/user")
  public map<string, string> bar(@requestheader("api-version") string apiversion, user user) {
    map<string, string> map = new hashmap<>();
    map.put("test", mybean.bar());
    map.put("version", apiversion);
    map.put("username", user.getname());
    //todo your business
    return map;
  }
}

参数设定为name=felord.cn&age=18,那么对应的http报文是这样的:

get /foo/user?name=felord.cn&age=18 http/1.1
host: localhost:8888
api-version: v1

可以预见的返回值为:

{
  "test": "bar",
  "version": "v1",
  "username": "felord.cn"
}

事实上对接口的测试可以分为以下几步。

构建请求

构建请求由mockmvcrequestbuilders负责,他提供了请求方法(method),请求头(header),请求体(body),参数(parameters),会话(session)等所有请求的属性构建。/foo/user接口的请求可以转换为:

mockmvcrequestbuilders.get("/foo/user")
.param("name", "felord.cn")
.param("age", "18")
.header("api-version", "v1")

执行mock请求

然后由mockmvc执行mock请求:

mockmvc.perform(mockmvcrequestbuilders.get("/foo/user")
.param("name", "felord.cn")
.param("age", "18")
.header("api-version", "v1"))

对结果进行处理

请求结果被封装到resultactions对象中,它封装了多种让我们对mock请求结果进行处理的方法。

对结果进行预期期望

resultactions#andexpect(resultmatcher matcher)方法负责对响应的结果的进行预期期望,看看是否符合测试的期望值。参数resultmatcher负责从响应对象中提取我们需要期望的部位进行预期比对。

假如我们期望接口/foo/user返回的是json,并且http状态为200,同时响应体包含了version=v1的值,我们应该这么声明:

resultmatcher.matchall(mockmvcresultmatchers.status().isok(),
mockmvcresultmatchers.content().contenttype(mediatype.application_json),
mockmvcresultmatchers.jsonpath("$.version", is.is("v1")));

jsonpath是一个强大的json解析类库,请通过其项目仓库https://github.com/json-path/jsonpath了解。

对响应进行处理

resultactions#anddo(resulthandler handler)方法负责对整个请求/响应进行打印或者log输出、流输出,由mockmvcresulthandlers工具类提供这些方法。我们可以通过以上三种途径来查看请求响应的细节。

例如/foo/user接口:

mockhttpservletrequest:
   http method = get
   request uri = /foo/user
    parameters = {name=[felord.cn], age=[18]}
     headers = [api-version:"v1"]
       body = null
  session attrs = {}

handler:
       type = cn.felord.xbean.config.foocontroller
      method = cn.felord.xbean.config.foocontroller#urlencode(string, params)

async:
  async started = false
   async result = null

resolved exception:
       type = null

modelandview:
    view name = null
       view = null
      model = null

flashmap:
    attributes = null

mockhttpservletresponse:
      status = 200
  error message = null
     headers = [content-type:"application/json"]
   content type = application/json
       body = {"test":"bar","version":"v1","username":"felord.cn"}
  forwarded url = null
  redirected url = null
     cookies = []

获取返回结果

如果你希望进一步处理响应的结果,也可以通过resultactions#andreturn()拿到mvcresult类型的结果进行进一步的处理。

完整的测试过程

通常andexpect是我们必然会选择的,而anddo和andreturn在某些场景下会有用,它们两个是可选的。我们把上面的连在一起。

@autowired
mockmvc mockmvc;

@sneakythrows
@test
void contextloads() {
   mockmvc.perform(mockmvcrequestbuilders.get("/foo/user")
      .param("name", "felord.cn")
      .param("age", "18")
      .header("api-version", "v1"))
      .andexpect(resultmatcher.matchall(status().isok(),
          content().contenttype(mediatype.application_json),
          jsonpath("$.version", is.is("v1"))))
      .anddo(mockmvcresulthandlers.print());
      
}

这种流式的接口单元测试从语义上看也是比较好理解的,你可以使用各种断言、正例、反例测试你的接口,最终让你的接口更加健壮。

5. 总结

一旦你熟练了这种方式,你编写的接口将更加具有权威性而不会再漏洞百出,甚至有时候你也可以使用mock来设计接口,使之更加贴合业务。所以crud不是完全没有技术含量,高质量高效率的crud往往需要这种工程化的单元测试来支撑。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。