自动化工具 一个基于多接口的业务自动化测试框架

陆压 · 2023年12月19日 · 最后由 weikjio 回复于 2024年02月08日 · 25911 次阅读
本帖已被设为精华帖!

这是一个成熟的框架,不是要让别人当小白鼠,它已经先后在两家公司的 5 条业务线进行了推广应用,用例条数到了几千条以上,并且从 18 年开始每天都在 CI/CD 流程中被调用执行。

已有那么多接口测试框架,为什么重复造轮子?首先,本框架如题目描述,适用于多接口的业务自动化测试,不是简单的接口测试框架;其次框架始于 17、18 年,当时也没有现在如此多的接口测试框架。

代码地址

框架介绍

接口自动化测试无疑是测试提效最为行之有效的方案,市面上的接口自动化测试框架很众多,而本框架与其它框架的区别如以下:

  • 用例代码编写简单,让使用者精力集中在所测试系统的业务逻辑上,而 http 接口的定义,请求的发送,测试报告信息等都由框架完成
  • 不只适用于单个接口的测试,同样适用于多个接口组成的完整的业务逻辑的测试,这往往是接口自动化测试更应该做到的
  • 登录等前置的业务操作也由框架完成,用例中只需引用相应 cookie
  • 框架同样支持环境、各类账号以及其它测试物料信息维护
  • 简单易用,java 小白也能在半小时内学会使用

框架结构

上手指南

工程结构说明

下面是一个论坛登录、浏览帖子、帖子点赞这样一个简单的业务场景进行举例,如何用框架完成这一几步操作的

定义 http 接口

接口定义是在 yml 文件中,建议按照被测系统维护 yml 文件

api:
  globalVariables:
    - UA: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36"
  pioneers:
    - name: testerhome登录
      id: testerhomeLogin
      priority: 1
      path: https://$testerhomeHost/account/sign_in
      method: post
      headers: >-
        \{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8","User-Agent":"$UA","x-requested-with":"XMLHttpRequest",
        "cookie":"user_id=eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik1UVTJOamM9IiwiZXhwIjpudWxsLCJwdXIiOiJjb29raWUudXNlcl9pZCJ9fQ%3D%3D--43f5d4f117b5459e67c85cc6c569820abb1e6068; _homeland_session=Y2ljEAtdhRcbEHaTSSHMb3%2FUyn0aLrFrHoEP8QVjVq%2BvXMCEi9n57WDgHBw40L%2Bo%2Fghe148%2B%2B429DbYDWNAiC4FBFYFnEghtzkQWPpKsOm21DZQkUDLvYqr4Z2ylpkiGHqjpppkhw0LLke61psEh7ZKQte3Ia3TTzTSu9ifDtHEl9FBlZUXNgwi%2F5kscioZqkobTyJpCGp5M4mSrLiunIZUHbgm05AuWa5%2Bu2TwgsxOfpdAumg6Q0SoT7ipMLaGaprobuP0Kj2q5ZH4CKqG7fb%2FU0WwzsTgTCtMXaWLz5WYHizGKRD5CWysSMseGn5I%3D--5LouY27EpiVkGarr--tpTXhgdFShw4Qyn6sThkpg%3D%3D",
        "x-csrf-token":"zr6fgSyPS5nyqcwGdzD7R6T51aAK6L9Dv42Lao0CSPZo4jEn3pT5fNN2eTk84VdmqhzQasF+sdHQrvvxsLYSmg=="\}
      parameters: user[login]=&user[password]=&user[remember_me]=0&user[remember_me]=1&commit=登录
      extractors: \[{"name":"token","value":"cookies"}\]
  requests:
    - name: 读帖子
      id:  topics
      path: https://$testerhomeHost/topics/38484
      method: get
      headers: >-
        \{"User-Agent":"$UA","Content-Type":"application/x-www-form-urlencoded","cookie":"$token","x-requested-with":"XMLHttpRequest","x-csrf-token":"r3E8899sEAEnqST2dmtIEluqG5C/nL/Rwp2l4ITtNDU3XpF4eULhClMRoWweMt6XWSmBn2H8fmPRas+CVkA/BA=="\}
    - name: 点赞
      id:  likes
      path: https://$testerhomeHost/likes
      method: post
      headers: >-
        \{"User-Agent":"$UA","Content-Type":"application/x-www-form-urlencoded","cookie":"$token","x-requested-with":"XMLHttpRequest","x-csrf-token":"r3E8899sEAEnqST2dmtIEluqG5C/nL/Rwp2l4ITtNDU3XpF4eULhClMRoWweMt6XWSmBn2H8fmPRas+CVkA/BA=="\}
      parameters: type=Topic&id=38484

如上,接口定义文件大体分为三部分:globalVariables,pioneers,requests。

  • globalVariables:定义全局变量,为 key、value 形式
  • pioneers 定义前置接口,用于定义登录等前置接口。程序启动后、用例开始执行前,会自动先执行 pioneers 中定义的接口。 其中 name 随意起;id 要唯一,建议按照接口请求地址的缩写命名 id 属性;priority,整数类型,当 pioneers 中定义了多个接口,执行时会按照 priority 属性排序,之后顺序执行。extractors:接口返回内容的提取,name,为提取的变量命名,后面接口可以通过 $name 名对其进行引用;value,变量的提取内容,支持提取 cookie 或返回 json 字符串中的某个属性 (填写属性的 json path)
  • requests 定义接口,基本同 pioneers 部分,少了 extractors 部分。

说明:此处的接口请求参数可以通过抓包工具抓包获取,然后复制到这里。接口定义只需定义一次,在用例中随意获取,使用接口时,根据需要设置请求参数,未设置的请求参数按照此处定义的值作为默认值。

用例代码:

@Test(enabled = true, description = "打开帖子详情页→点赞")
public void test() {
    log.info("test start");
    //请求实例1,打开帖子详情页
    Request request = Request.getInstance("topics");
    //请求1发送
    Response response = request.doRequest();
    //返回为html,取其中的x_csrf_token,后面点赞接口用
    String html = response.asString();
    Headers  headers = response.getHeaders();
    Map<String, String> cookies = response.getCookies();
    Document document = Jsoup.parse(html);
    Element metaElement = document.select("meta[name=csrf-token]").first();
    String x_csrf_token = null;
    if (metaElement != null) {
        x_csrf_token = metaElement.attr("content");
    }
    //请求实例2,点赞接口
    request = Request.getInstance("likes");
    //更新cookie
    request.addCookies(cookies);
    if (x_csrf_token != null) {
        request.addHeader("x-csrf-token",x_csrf_token);
    }
    //发送点赞请求
    response = request.doRequest();
    assertThat(response.getStatusCode()).isGreaterThanOrEqualTo(200).as("返回状态码校验");
}

测试报告

如下图,用例相关接口的请求信息、返回信息也都由框架自动记录在了报告中,如有其它需要内容输出到测试报告,可以在用例中添加 Report.log("要添加内容");

其它

  • 配置:如其它 spring 工程,配置文件在 resources 目录下,类似 pre、test 区分不同环境,application.properties 中定义一般的配置信息(和环境无光),其中 pring.profiles.active=pre 来切换不同环境

  • 测试范围定义:测试用例由 testng 维护,如框架中所示,详细使用方法参见 testng 官网

    <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
    <suite verbose="1" name="bulls-test" >
    <listeners>
        <listener class-name="com.bulls.qa.service.CustomListener"></listener>
        <listener class-name="com.bulls.qa.service.NoticeListener"></listener>
    </listeners>
    <test name="bulls自动化" preserve-order="true">
        <parameter name="reruntimes" value="0"></parameter>
        <packages>
        </packages>
        <classes>
            <class name="com.bulls.qa.testcase.testerhome.Demo">
                <methods>
                    <include name="test"></include>
                </methods>
            </class>
        </classes>
    </test>
    </suite>
    
  • 运行:项目入口 com.bulls.qa.BullsApplication.main

//打包
mvn clean -DskipTests=true  package
//运行
java -jar target/bulls-0.6-SNAPSHOT.jar  测试范围配置文件.xml  

如上面例子,测试范围配置文件可以配置多个,执行时指定测试范围,如不指定默认使用打包的程序代码中的测试范围配置文件

  • 测试报告:测试报道为单 html 文件,方便 jenkins 配置展示,报告地址运行时所在目录下 bulls.html
  • 断言,选用的断言框架为 AssertJ,AssertJ 的强大无需赘述,详细使用方法参见 AssertJ 官网
assertThat(response.jsonPath().getList("recommendations")).size().isGreaterThan(0).as("recommendations长度大于0");
assertThat(response.jsonPath().getBoolean("has_more")).isTrue().as("has_more为true");
assertThat(response.jsonPath().getList("recommendations")).as("recommendations长度大于0").size().isEqualTo(3);
List<String> types = JsonPath.from(response.asString()).getList("recommendations.item_type");
String[] strs = "product,product-ad-card,deal,ad,shopping-curated-collection,auto-generated-collection,video,campaign-banner,benefit,web-view".split(",");
assertThat(strs).containsAll(types).as("types在枚举范围内");
  • 发送测试结果消息通知,参见代码 NoticeListener,具体根据需要自行扩展
  • 接口传参设置,较复杂的接口参数设置

相关接口定义

- name: 编辑商品
  id: itemEdit
  path: http://$mnghost/item/edit
  method: post
  cookies: $XXXXXXCookies
  headers: >-
    \{"User-Agent":"$UA","Content-Type": "application/json"\}
  parameters: >-
    \{"itemId":"2904"\}
- name: 添加商品
  id: itemSave
  path: http://$mnghost/item/save
  method: post
  cookies: $XXXCookies
  headers: >-
    \{"User-Agent":"$UA","Content-Type": "application/json"\}
  parameters: >-
    \{"itemId":"2913","categoryIdList":[1],"topCategoryName":"美食","itemName":"autoTest goods","limitNumber":3,
    "priceText":"","countDownCycle":"3","countDownLimit":"1","itemNo":"12sqw","delivery":"MANUAL",
    "image":"//yun.XXXXXX.com/images/202005/4su03vvahd.jpg","detail":"","itemStatus":"ON","skuProperties":[],
    "skuList":[{"id":3375,"stock":999999,"stockId":null,"sellingPrice":100,"originalPrice":100,"costPrice":100,
    "realPayPrice":100,"properties":null,"skuNo":"1","skuEnable":true,"changeStock":0}],"supportCOD":true,
    "originItemId":null,"merchantId":73,"tagIds":[],"id":2913,"topCategoryId":1,"itemShortName":"autoTest goo","url":null,
    "minPrice":100,"stock":0,"isRecommend":false,"minSkuOriginalPrice":null,"minSkuPriceDiff":null,"maxPriceDiff":null,
    "maxPriceDiffPrice":null,"maxPriceDiffOriginalPrice":null,"gmtModified":"2020-06-19 16:57:36","gmtModifyName":"测试专用",
    "gmtModifyEmail":"test@XXXXXX.com.cn","mainRecomIds":null,"merchantName":"autoTestShop01","merchantDelivery":"MANUAL",
    "imgHeight":[{"imgUrl":"http://yun.XXXXXX.com/images/202006/mj3yg07pj8.jpg","height":136},
    {"imgUrl":"http://yun.XXXXXX.com/images/202006/d47ad68hhc.jpg","height":372}],"mainImgUrl":null,"itemIntroduce":null,
    "saleLableUrl":null,"ssoDesc":null\}

相关代码

goodsId = 2904;
//编辑接口,获取测试的商品信息
Request request = Request.getInstance("itemEdit");
//直接设置,key-value形式
Response response = request.setParameter("itemId", goodsId).doRequest();
//库存小于50,更新库存
JsonPath jsonPath = response.jsonPath();
if (jsonPath.getBoolean("success") && jsonPath.getInt("data.stock") >= 50) {
    // dosomething
}
Map<String, Object> map = response.jsonPath().getMap("data");
if (map == null) {
    map = new HashMap<>();
}
map.put("itemId", goodsId);
map.put("stock", 9999999);
request = Request.getInstance("itemSave");
//遍历接口的传参结构定义,替换掉key完全匹配的那个map部分
request.setParameters(map);
//按照json path定位要设置的key
request.setParameter("$.skuList[0].stock", 9999999);
request.setParameter("$.skuList[0].changeStock", null);
//根据路径删除,路径按json path
request.removeParameterByPath("$.skuList[0].stockId");
request.removeParameterByPath("$.skuList[0].id");
request.doRequest();
共收到 11 条回复 时间 点赞
恒温 将本帖设为了精华贴 12月20日 09:14

有点意思,这个是不是加上工作流编辑会更好。

java 我只是略懂,虽然看结构图感觉很高大上,但是细看下是不是就是比较通用的【yaml 数据驱动方式自动化测试】: 用 python 可以省至少一半的代码😂

只看了 Demo 的 testcase,你的思路大概是 先创建了一个 Request 实例,用于发起请求。然后,通过 doRequest() 方法发起请求,获取响应。接着,从响应中获取一些信息,如 HTML、请求头和 Cookie。然后,创建了一个新的 Request 实例,用于发起另一个请求。通过 addHeaders() 和 addCookies() 方法向请求中添加请求头和 Cookie。最后,通过 doRequest() 方法发起请求,并使用 assertThat() 方法进行响应状态码的校验😂

在参数化测试方法中,也是先创建了一个 Request 实例,用于发起请求,通过 setParameter() 方法向请求中添加参数。接着,通过 doRequest() 方法发起请求,并获取响应。最后使用断言方法进行响应状态码的校验😂 中间还有使用读取 excel 文件作为参数数据源。

如果用性价比平替的方式,我建议还是用 pytest😁 ,第一种方式使用 python Request 的 Session 对象发送请求。第二种用 pytest 的@parametrize装饰器为数据提供参数化测试,然后还是用 assert 语句断言检查。数据驱动的方式也是 yaml 文件,就用 pyyaml,测试报告用 allure

"不只适用于单个接口的测试,同样适用于多个接口组成的完整的业务逻辑的测试,这往往是接口自动化测试更应该做到的"

真没见过谁的接口自动化测试是只能做单个接口的,还有,定义了 yaml 还得在写一个 test 类型的代码,一个接口写两次。。。。

测试新人 回复

1.yml 文件维护的接口定义信息(不是为做数据驱动),也就是接口请求参数的格式,一般是 json,当然表单、get 传参、文件上传最终的二进制文件都没问题;目的一方面是方便接口定义维护,再者避免掉接口请求时设置地址、登录 cookie、参数组织(业务中往往参数结果很复杂)这些重复冗余代码的,后面用例代码只需根据测试意图设置相应的参数值,未设置的参数值按照定义中的值作为默认,写用例时更多关注业务逻辑,也就是接口间的关系就好了
2.场景用例很多时候不只是会话保持 - 把前面请求的 cookie 带进去,如 demo 中 testerhome 帖子点赞的例子,整个流程是登录(前置操作中完成)- 浏览帖子 - 点赞,最后的点赞除正常参数外,还需要 cookie 中带一个 x_csrf_token,这个 csrf_token 是在浏览帖子的 html 中动态传递的,另外点赞接口还需要一个 cookie 也是从浏览帖子的返回中获取的,如 1 中表达,这种接口间的关系是测试用例中的重点,而其它接口调用需要的信息由框架来解决;这个例子断言只校验了状态码,只是示例,当然也可以校验点赞前后点赞数的变化,其落库情况等,不是这里表达的重点,没有对此展开
3.编程语言的选择,是因为组内多数人更熟悉 java,pytest 和 testng 同样优秀,就像 python 之余 java,java 中的 http 请求框架很多也支持会话的,编程效率上有差异,但不是很大,不用过分纠结;allure 也用过一段时间,当时它有些 bug 不知后面版本 fix 没有
4.总之,这个框架解决的是接口调用的简洁性问题

嗯……,很多是靠维护用例之间的执行顺序来测试业务逻辑的,这样在用例较多或业务逻辑逻辑复杂时候,维护起来很麻烦;第二个问题没看明白,没有一个接口写两次呀

陆压 回复

😂 不是纠结于哪种语言,只是看到原来用 Java 实现的接口自动化和 python 实现的差不多,但是代码居然能多出这么多感到惊讶😂

陆压 回复

不会的,Java 的不知道怎么弄,但是普遍是用 pytest 的话,fixture 是可以创建测试环境的前置条件和后置条件,确保每个测试用例都在独立的环境/测试数据中运行。pytest.mark.dependency 和 pytest.mark.dependency(depends) 标签,可以指定测试用例的依赖关系。至于维护,其实也是用 yaml 写的测试数据,差不多的

楼主 demo 有开源吗

不开源就不要放上来了

python 版本,毛遂自荐:https://github.com/wu-clan/httpfpt
作为开源项目,已被多名小伙伴在公司落地实施

推荐 puppy-test,易用易上手,可直接 pip 安装

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