研发效能 API 自动化的快速实施与落地,最后有彩蛋

尼古拉斯 · 2021年06月09日 · 最后由 simonpatrick 回复于 2021年06月09日 · 525 次阅读

API 自动化的快速实施与落地

内容简介:通过一个简单的实际案例,介绍如何使用 PnxTest 快速的实施 API 自动化测试,以及 API 自动化过程中一些常见的问题处理方法
使用框架:开源的 PnxTest 测试框架
阅读对象: API 自动化测试人员、测试管理人员
阅读时长: 约 5 分钟

 

理解需求

在正式开始测试之前,深刻理解我们的测试目标、具体的技术实现方案是必须的,有助于我们设计出覆盖全面的测试用例。通常有以下几种途径:

  • 产品设计文档

  • API 接口设计文档

  • 时序图

案例:所有的接口必须有签名以及 token,否则验签与 token 权限不通过,请求将失败。认证使用 jwt token, Bearer 方式;签名规则如下:

/* API请求签名
 * ===============
 * 签名原文规则:
 * ===============
 * URI(去域名(baseUrl),去左右空格,全小写) +
 * QueryString(key&value, 按key升序,去左右空格,全小写) +
 * Header(key&value,只取[device-id,timestamp,token]升序,去左右空格,全小写) +
 * Body raw(去左右空格,全小写)
 * 或者 form-data(key&value,按key升序,去左右空格,全小写)
 * 或者 x-www-form-urlencoded(key&value,按key升序,去左右空格,全小写)
 * ===============
 * 签名算法:
 * ===============
 * 根据自己公司的接口签名算法来进行加密处理(这里的例子我使用简单的base64来演示,没有私用私钥)
 *
 */

 

用例设计

在正式开始编码之前,先进行用例设计并输出测试设计文档 (test specification)。从测试管理和流程上看,这将是一个非常重要的环节:
1)测试设计文档作为输出物,体现了测试人员/团队的产出与智慧。作为具体自动化代码的指导,Review(用例评审) 通过后才能进行具体的代码实施。
2)测试设计文档在团队人员变动、智慧传承方面也会起到显著的作用。

 

自动化工程

了解完需求以及测试设计文档也经过与产品、开发 review 通过后,就进入具体的自动化 coding 阶段了。这里我选用 PnxTest 框架来开展,简洁&操作流畅。

1、打开 Intellij Idea, 创建一个新的 Maven 项目:项目名称根据自己的需要填写,这里我用 api-testing-example

2、pom 文件中增加 pnxtest 依赖

3、根据官方推荐的项目文件结构,创建相关文件夹

4、配置环境

环境 描述 Base Url Database
qa 测试环境 http://10.10.21.10/api mysql 数据库, 地址&端口:10.10.20.5:3306
pre 预发布环境 https://pre.pnxtest.com/api mysql 数据库, 地址&端口:10.20.30.6:3306
prod 生产环境 https://pnxtest.com/api mysql 数据库, 地址&端口:10.30.40.7:3306

这里有三个环境, 其中 qa 环境不需要 https. 在 test-config 文件夹下创建三个 properties 文件:qa.env.properties pre.env.properties prod.env.properties 写入相应的配置内容,以 qa 测试环境为例:

pnx.http.baseUrl = http://10.10.21.10/api 

pnx.db.url=jdbc:mysql://10.10.20.5:3306/db_account
pnx.db.driver=com.mysql.cj.jdbc.Driver
pnx.db.user=pnxtest
pnx.db.password=secret.sXI4yXOv1TC5nfH4
pnx.db.timeout=5
pnx.db.timezone=GMT+8

https.required = false;

4、由于所有接口都要验证签名与 token 权限,因此使用@Configuration进行统一网关配置。

创建一个类,名字可以任意。我这里为 HttpGatewayConfig:

@Configuration
public class HttpGatewayConfig implements IHttpConfig {
    @Override
    public com.pnxtest.http.HttpConfig accept() {
        CustomHttpGateway myHttpGateway = new CustomHttpGateway();

        return HttpConfig.builder()
                .header("clientId", "PnxTest") //添加一个header
                    .header("token", "aeasfasfa.udfadsfafasfdasfdsfsfsdf") //添加token认证
                .connectionTimeout(5000) //设置连接超时时间5s
                .socketTimeout(5000) //设置读取内容超时时间
                .gateway(myHttpGateway) //设置统一网关
                .build();
    }
}  
class CustomHttpGateway extends HttpGateway{
  //request做统一拦截处理 
  @Override
  public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
    String url = httpRequest.getRequestLine().getUri();
    String reqMethod = httpRequest.getRequestLine().getMethod();

    try{
      StringBuilder signPlainData = new StringBuilder();
      URI uri = new URI(url);

      //handle uri
      String path = uri.getPath();
      String pathWithoutApi = path.replaceFirst("/api", "");
      signPlainData.append(pathWithoutApi);

      //handle queryString
      Map<String, List<String>> queryStrings = parseQueryString(uri.getRawQuery());
      List<String> queryStringKeys = new ArrayList<>(queryStrings.keySet());
      Collections.sort(queryStringKeys); //升序排列
      for(String key: queryStringKeys){
        for(String v: queryStrings.get(key)) {
          signPlainData.append(key);
          signPlainData.append(v);
        }
      }

      //handle headers
      Header[] headers = httpRequest.getAllHeaders();
      Arrays.sort(headers, new Comparator<Header>() {
        public int compare(Header o1, Header o2){
          return o1.getName().compareToIgnoreCase(o2.getName());
        }
      });

      for(Header header:headers){
        String hName = header.getName();
        String hValue = header.getValue();
        if(hName.equalsIgnoreCase("device-id")
           || hName.equalsIgnoreCase("timestamp")
           || hName.equalsIgnoreCase("token")){
          signPlainData.append(hName);
          signPlainData.append(hValue);
        }
      }

      //handle body
      if (!reqMethod.equalsIgnoreCase(HttpMethod.GET.name())) {
        HttpEntity reqEntity = ((HttpEntityEnclosingRequest) httpRequest).getEntity();
        if (reqEntity != null && reqEntity.getContentLength() > 0) {
          String reqBody = convertInputStreamToString(reqEntity.getContent());
          if(!StringUtil.isEmpty(reqBody)) {
            String contentType = null;
            if(reqEntity.getContentType() != null){
              String[] contentTypes = reqEntity.getContentType().getValue().split(";", 2);
              contentType = contentTypes[0];
            }

            if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
              Map<String, List<String>> formdataMap = parseQueryString(reqBody.trim());
              List<String> fieldKeyList = new ArrayList<>(formdataMap.keySet());
              Collections.sort(fieldKeyList);
              for (String key : fieldKeyList) {
                for (String v : formdataMap.get(key)) {
                  signPlainData.append(key);
                  signPlainData.append(v);
                }
              }
            } else {
              signPlainData.append(reqBody);
            }
          }
        }
      }

      String plainText = signPlainData.toString(); //签名原文
      String sign = calculateSign(plainText); //加密签名
      httpRequest.addHeader("sign", sign); //添加签名到http请求头

    }catch (IOException | URISyntaxException e){
      //
    }
  }

  @Override
  public void process(HttpResponse httpResponse, HttpContext httpContext) throws HttpException, IOException {
    //这里可以response做统一拦截处理
  }


  private String calculateSign(String plainText) {
    //根据自身公司算法来计算,这里只是一个简单base64加密, 并没有使用私钥
    return Base64.getEncoder().encodeToString(plainText.toLowerCase().getBytes(StandardCharsets.UTF_8));

  }

  private Map<String, List<String>> parseQueryString(String queryString) throws UnsupportedEncodingException {
    if(StringUtil.isEmpty(queryString) || StringUtil.isBlank(queryString)){
      return new LinkedHashMap<String, List<String>>(0);
    }

    final Map<String, List<String>> queryPairs = new LinkedHashMap<String, List<String>>();
    final String[] pairs = queryString.split("&");

    for (String pair : pairs) {
      final int idx = pair.indexOf("=");
      final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair;
      if (!queryPairs.containsKey(key)) {
        queryPairs.put(key, new LinkedList<String>());
      }

      final String value = (idx > 0 && pair.length() > idx + 1) ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") : null;
      queryPairs.get(key).add(value);
    }

    return queryPairs;
  }

  private String convertInputStreamToString(InputStream inputStream){
    try {
      ByteArrayOutputStream result = new ByteArrayOutputStream();
      byte[] buffer = new byte[1024];
      for (int length; (length = inputStream.read(buffer)) != -1; ) {
        result.write(buffer, 0, length);
      }
      result.close();
      return result.toString("UTF-8");
    }catch (IOException e){
      //ignore
    }

    return "";
  }
}

5、一切准备就绪,开始编写测试用例:

@Controller(module = "项目模块", maintainer = "陈鹏")
public class ProjectCategoryTest {
    @Test
    @DisplayName("获取指定项目下的所有分类列别,树形结构")
    void projectCategoryListingTest(){
        HttpResponse<String> response = PnxHttp.get("/api/v1/tms/{programId}/testCategory/")
                .routeParam("programId", "1")
                .asString();

        PnxAssert.assertThat(response.getStatus())
                .as("非法token请求")
                .isEqualTo(401);
    }

}

6、 IDE 中调试运行:设置运行环境为 qa, 执行,测试通过,完美!到test-outputting文件夹下查看测试报告:签名有了,详细测试日志也有了, perfect!

7、把项目打成一个可执行 jar, 放到 CI(如 jenkins) 中运行。

 

总结说明

1、该演示工程代码已 push 到 GitHub:https://github.com/pengtech/api-testing-example 里面的 Base URL 地址,数据库链接地址,请修改为自己内部可用的地址

2、上面的示例演示了如何统一签名、统一认证, 以及如何忽略或者开启 https 认证,动态路由等自动化测试中常见的问题

3、 数据库的连接密码属于敏感信息,上面的示例直接在配置文件中暴露密码原文,那如何进行加密处理呢?可以参考官方文档

4、上面的示例只演示了一个测试用例,当测试的接口和用例比较多时,如何组织与复用呢? 使用@Repository @Steps

 

最后的彩蛋

人工智能与机器学习 这几年在测试领域也得到了不错的应用,效果显著。作为一个与时俱进的自动化测试框架,使用 AI/ML 来增强自动化测试不可或缺,对于 API 自动化这块,后续版本将提供 “通过 ML 生成自动化测试场景用例” 功能,目前的进展和内部的测试效果,ML 可以覆盖大约 30%-40% 的测试用例。

共收到 1 条回复 时间 点赞

突然发现这个和我很久以前的框架其实思路差不多的, http://testerhome.com/topics/3690
其实都不需要打包放 jenkins 的,直接使用 maven 命令就可以跑了.

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册