最近问接口测试的人挺多. 我先把我们公司自己设计的接口测试框架的 README.md 文档贴出来. 供大家做个思路参考. 工具会在过阵子开源出来. 目前是 100% 的用例都来自于录制并生成的测试用例. 然后稍作修改和调整断言.

HttpApi 基于 Http 的接口自动化框架

使用介绍

#下载工具解压进入目录

#开启代理.监听8788端口
bin/httpapi proxy 8788

#配置浏览器代理或者移动设备代理. 移动端需要导入证书.然后进入手工测试.  

#从录制的Har文件中导出测试用例到scala文件, 比如只过滤匹配url包含search或者list的接口数据
bin/httpapi export proxy.har search list 

#编辑测试用例, 并放入到src/test/scala/下即可 

#运行所有用例
bin/httpapi test
#运行部分测试用例
bin/httpapi test-only com.xueqiu.httpapi.trade.*

录制并生成测试用例

框架会利用代理录制出来基本的测试用例框架.

生成的测试用例有三种风格. csv dsl 和 api. 推荐使用 dsl

api 方式

这是标准的 scala 调用方式, 可以自由的编码, 适合熟悉 scala 的人. 也可以导入到 java 的 maven 项目中使用

目前因为精力有限, 一直没有给研发打包.

dsl 方式

这是较为规整的语法结果, 以 | 作为分隔符的类 table 结构. 是 scala 的内部 dsl 风格.

csv 方式

这是纯 csv 方式,不过 csv 自身的语义表达太弱, 无法处理太复杂的结构. 所以不建议使用. 你可以从部分的样例中了解它的细节用法.

Excel 也不能做逻辑处理. 还是离不开语言的 runtime 支持.

总体推荐使用 dsl 风格

DSL 方式编写测试用例

这是一个验证雪球券商账号绑定和解绑的简单测试流程示例

test("多个账户绑定"){
  data{
    "tid" | "account" | "password"
    "PINGAN" | "account_demo " | "password_demo"
  }
  flow2() {
    "Authorize" | "req" | "tid"              | row("tid")
    "Authorize" | "req" | "fund_account"     | row("account")
    "Authorize" | "req" | "account_password" | row("password")
    "Authorize" | "res" | "result_code" | "60000"

    "data/proxy.har" |"Bind" |"req" | "tid"| row("tid")
    "Bind" | "res" | "result_code" | "60000"

    "data/proxy.har" |"Unbind" |"req" | "tid"| row("tid")
    "Unbind" | "res" | "result_code" | "60000"
  }

flow2 关键词

这是一个只有四列数据的 dsl. 可以省略掉通用的 proxy.har. 适合 har 文件不变的情况
中文 dsl 不过是中文的 class 而已. flow2 是 flow 关键词的简化版, 已经不推荐使用原来的 flow 关键词了

test("中文Dsl"){
  flow2("proxy.har") {
    "登录" | "req" | "username" | ""
    "登录" | "req" | "telephone" | "1560053xxxx"
    "登录" | "req" | "password" | "password"
    "认证" | "req" | "tid" | "PINGAN"
    "认证" | "req" | "fund_account" | "account"
    "认证" | "req" | "account_password" | "account_password"
    "认证" | "res" | "result_code" | "60000"
  }  
}

flow2 的参数为你录制的 har 文件地址, 可以是目录. 他会读取里面的每个 http 接口, 分析结构的请求和响应.

可以通过 httpapi 工具的录制模式或者 chrome fiddler 等工具生成. 里面的每一行代表了接口请求的构建和结果校验

请求数据会被当做接口请求的默认值, 响应数据会被当做期望值去校验

第一列为接口指定, 他支持两种定义格式. 命名接口和 url

第二列为操作选项, 有若干选项. req config path res 和 send.

第三列为取值方式

第四列为设置值

第三列表示真实值的获取方式, 支持 jsonpath kv 结构和回调函数

第四列表示期望值或者设置值是什么

主要是如下的用法

url | req | key | value 
url | res | actual | expect 

默认接口 url 命名接口

默认录制的测试用例会使用接口的 path 部分来指定使用具体的接口.

接口的定义是从录制生成的 json 格式的 har 文件中寻找定义.

url 格式为去掉前阵和后缀的路径, 比如

flow2("proxy.har"){
      "/url/demo/path/api" | "req" | "trace_id" | "trace_id_demo"
      "/url/demo/path/api" | "req" | "client" | "1"
      "/url/demo/path/api" | "req" | "_" | "1441611982405"
      "/url/demo/path/api" | "req" | "access_token" | "token_demo"
      "/url/demo/path/api" | "req" | "x" | "0.24"
      "/url/demo/path/api" | "req" | "access_token_expires" | "-1"

      "/url/demo/path/api" | "res" | "msg" | "操作成功"
      "/url/demo/path/api" | "res" | "result_code" | "60000"
    }

他会从 proxy.har 中寻找路径符合/url/demo/path/api 的接口, 并把请求值初始化.

然后再根据你写的用例吧具体的参数值重新赋值. 这样的好处就是测试用例可以不用写太多赋值语句了.

config 命名接口

"User" "Authorize"为预定义好的关键词. 表示某个固定的接口, 这类接口一般是经过了特定的处理. 比如制定了接口的 url method, 预先初始化参数值等.

比如券商有十几种接口. 通过"Authorize"只需要指定这个接口的关键数据即可, 他会根据你填写的券商数据自动选择各家券商针对的接口 url 地址.
定义命名接口有 2 个形式. 直接写一个 scala 的类定义. 或者直接在测试用例中使用如下形式来指定

"baidu" | "config" | "url" | "http://www.baidu.com/not_exist?wd={a}/{b}/"
"baidu" | "config" | "method" | "get"

req

req 表示是对请求的构造, 他回把后面的数据当做 key 和 value 添加请求中. 请求的构造会根据 har 文件中的请求的方式进行构建.

比如 get 方式就构建 url?key=value&...

post 方式就构建 request body.

将来也会支持 json 格式和 xml 格式等.

如下例子会发送一个http://www.baidu.com/s?wd=mp3&time=2015请求

"baidu" | "config" | "url" | "http://www.baidu.com/s"
"baidu" | "config" | "method" | "get"
"baidu" | "req" | "wd" | "mp3"
"baidu" | "req" | "time" | "2015"

如下例子发送 post 数据 wd=mp3&time=2015 到http://www.baidu.com/s

"baidu" | "config" | "url" | "http://www.baidu.com/s"
"baidu" | "config" | "method" | "post"
"baidu" | "req" | "wd" | "mp3"
"baidu" | "req" | "time" | "2015"

path

有些接口比如 REST 风格的接口, 有些变量是在 url 路径里面, 比如/user/{id}, 所以增加了这个 path 的支持.

通过这样的 dsl 可以设定 url 中的变量数据

比如以百度的接口为例

"baidu" | "config" | "url" | "http://www.baidu.com/{a}/{b}/"
"baidu" | "config" | "method" | "get"
"baidu" | "path" | "a" | "1"
"baidu" | "path" | "b" | "xxx"

res

res 表示把后面的 key|value 当做期望值进行比较.

key 支持 jsonpath 和普通的 key 方式. 比如结果格式为

{
  "result_code" : "60000",
  "msg" : "",
  "result_data" : [ ],
  "total_income_balance" : 0,
  "day_income_balance" : 0,
  "day_income_rate" : null,
  "total_income_rate" : null
}

判断 result_code 可以写成如下两种风格. 当然推荐用 JsonPath

"/url/demo/path/api" | "res" | "result_code" | "60000"
"/url/demo/path/api" | "res" | "$.result_code" | "60000"
"/cubes/discover/user/recommend" | "res" | "$.[0].recommend_reason" | "雪球投资达人"

JsonPath 语法参考 https://github.com/jayway/JsonPath

send

这个大多数情况并不需要使用. 在断言 res 的时候会只能发送请求.

只有没有断言或者没有后续其他请求的时候, 才需要使用 send 显式的发送请求

数据参数化与数据驱动 data 关键词

data 关键词可以实现数据参数化和数据驱动. flow 则是定义了业务流程.

比如在券商的接口测试流程中, 想添加更多的数据集合, 但是又不写重复的测试步骤. 那么就可以利用 data{}和 row 来实现

第一行为表头, 其他行每行数据为一个集合. 会驱动 flow{}完整的运行一次.

test("多个账户绑定"){
  data{
    "tid" | "account" | "password"
    "PINGAN" | "account_demo" | "account_password"
    "FZZQ" | "xxx" |"ddd"
  }
  flow2("data/proxy.har") {
    "Authorize" | "req" | "tid"              | row("tid")
    "Authorize" | "req" | "fund_account"     | row("account")
    "Authorize" | "req" | "account_password" | row("password")
    "Authorize" | "res" | "result_code" | "60000"

    "Bind" |"req" | "tid"| row("tid")
    "Bind" | "res" | "result_code" | "60000"

    "Unbind" |"req" | "tid"| row("tid")
    "Unbind" | "res" | "result_code" | "60000"
  }

data{}中第一行表示每一列的表头. 以下每一行都是一组测试数据.

在 flow{}中用 row("name") 来替换原先硬编码的数据.

断言

除了标准的返回结构外, 还支持对 http 状态码 头信息, 接口的结构做断言.

test("302测试") {
   "baidu" | "config" | "url" | "http://www.baidu.com/not_exist?wd={a}/{b}/"
   "baidu" | "config" | "method" | "get"
   "baidu" | "path" | "a" | "1"
   "baidu" | "path" | "b" | "xxx"
   "baidu" | "res" | "_.code" | 302
   "baidu" | "res" | "_.headers.Server[0]" | "Apache"
   "baidu" | "res" | "_.code" | 404
 }

支持对接口的返回格式做校验, 对于每个返回结果, 都会对结构做解析, 并生成一个唯一 md5 签名.

如果结构发生变化, 这个签名就会发生变化 每次运行测试的时候都会在 log 中打印 res_schema_md5 的值.

可以找到对应接口的值并补充到自己的测试用例中. 如果接口结构变化, 记得更新即可.

"pankou" | "res" | "_.res_schema_md5" | "e4d000c6c9c4dae275cdc3ec53869e53"

在 flow 中 res 后面的最后两列表示真实值的访问方式和预期值. 默认是相等的关系.

如果自己想定义的更复杂的断言. 可以使用自定义断言. 比如

"Authorize" | "res" | "$.result_code" | { (actual:Any) => actual should equal ("80000") }

最后一列修改为 scala 的表达式, 这是一种匿名函数. 当然也可以写成命名函数, 这样可以编写更多复杂的处理.

val two_items_equal=(actual:Any) => actual should equal ("80000") 
"Authorize" | "res" | "$.result_code" | two_items_equal

常见的断言方式如下

## 字符串校验
string should startWith ("Hello")
string should endWith ("world")
string should include ("seven")
string should startWith regex "Hel*o"
string should endWith regex "wo.ld"
string should include regex "wo.ld"
## 数字校验
one should be < 7
one should be > 0
one should be <= 7
one should be >= 0

## 范围校验
sevenDotOh should equal (6.9 +- 0.2)
sevenDotOh should === (6.9 +- 0.2)
sevenDotOh should be (6.9 +- 0.2)
sevenDotOh shouldEqual 6.9 +- 0.2
sevenDotOh shouldBe 6.9 +- 0.2

## 可选值校验
Array(1) should contain oneOf (1,2,3)

所有支持的断言方式可参考 http://doc.scalatest.org/2.0/index.html#org.scalatest.Matchers

完整的测试用例

编写的测试用例本质是 scala 文件. 采用 scala 是因为它强大的表达能力.

所以在 case 的任何地方都可以直接写代码.

完整的测试用例如下

//定义suite名字
package com.xueqiu.httpapi.trade
//引入依赖包
import com.xueqiu.httpapi.framework.DslData._
import org.scalatest.{BeforeAndAfterAll, FunSuite}

//定义测试类和公共步骤
class QuanShangFlowMiniDemo extends FunSuite with BeforeAndAfterAll {
  //定义所有case的seetup. 只执行一次
  override def beforeAll(): Unit = {
    flow2("data/proxy.har") {
      "User" | "req" | "username" | username
      "User" | "req" | "password" | password
      "Authorize" | "req" |  "tid" | tid
      "Authorize" | "req" |  "fund_account" | fund_account
      "Authorize" | "req" |  "account_password" | account_password
      "Authorize" | "res" |  "result_code" | "60000"
    }
  }
  //固定编码方式
  test("历史") {
    flow2("data/proxy.har") {
      "list_history" | "req" | "_" | "1441611757463"
      "list_history" | "req" | "client" | "1"
      "list_history" | "req" | "x" | "0.154"
      "list_history" | "req" | "pos" | ""
      "list_history" | "req" | "end" | "1441611752611"
      "list_history" | "req" | "start" | "1439019752611"

      "list_history" | "res" | "msg" | ""
      "list_history" | "res" | "result_code" | "60000"
    }
  }

  //数据驱动方式
  test("多个账户绑定"){
    data{
      "tid" | "account" | "password"
      "PINGAN" | "account_demo_1" | "account_password_1"
      "PINGAN" | "account_demo_2" | "account_password_2"
      "PINGAN" | "account_demo_3" | "account_password_3"
    }
    flow2(){
      "User" | "req" | "username" | username
      "User" | "req" | "password" | password

      "Authorize" | "req" | "tid"              | row("tid")
      "Authorize" | "req" | "fund_account"     | row("account")
      "Authorize" | "req" | "account_password" | row("password")
      "Authorize" | "res" | "result_code" | "60000"

      "Bind" |"req" | "tid"| row("tid")
      "Bind" | "res" | "result_code" | "60000"

      "Unbind" |"req" | "tid"| row("tid")
      "Unbind" | "res" | "result_code" | "60000"
    }
  }
}

框架预定义变量

每个业务可以自己定义自己的 FunSuite 类环境. 用来定义自己业务的一些通用的操作.

比如定义全局变量 读取配置文件等.

tid 券商的tid. 
fund_account 券商账号
account_password= 券商账号密码

username 默认登录手机
password 默认登录密码

框架技术基础

json4s 用于实现 json 的数据解析和 json schema 的校验,官方有详细的文档

play-ws 实现 httpclient 的封装, 官方有大量的测试用例

gatling-recorder 用于实现自动录制接口请求并生成代码, 后来发现他的代码很挫, 就用了另外一个架构更好的 proxy lib.
scala-test 实现测试用例的管理和运行. 支持 junit rspec 和 BDD 的测试风格.

jenkins 实现持续集成结合

调试工具推荐

代理分析工具: charles, burp suite, windows 上的 fiddler. 推荐使用 burp.

设计思路

接口测试的核心因素是业务流程, 请求构造, 响应断言. 这些数据可来源于如下途径

通过代理技术可以获得上述所有数据, 是成本最低的办法. 缺点是只能开发完成后弥补接口

Swagger 定义了接口规范. 也包含上述数据, 但是缺少业务流程说明. 只适合资源型接口

War 包类反射. 从 War 或者 Jar 包中可以反射出所有的接口. 但是业务流程未知.

考虑到目前的现状, 先从代理技术入手. 完善现有业务的接口测试防护. 做好回归和冒烟测试.

然后再利用流量复制和录制来获知更多的业务接口使用场景

选择 scala 作为框架语言是因为这是 jvm 体系上最灵活的语言, 动态性比较强, 语法灵活.

而且 scala 的测试体系的产品比较完备, 比如常见的 gatling+zipkin+diffy+scalatest, 几乎覆盖了功能测试, 性能测试, 监控和数据分析.
所以最初就决定转向 scala 去设计这个框架, 语言并无好坏 关键是否适用场景

不过因为 scala 太过强大, 不利于新手学习. 建议先选择其他的语言作为过渡比较好.

TODO

支持更自由的语法结构, 使用 robotframework 的风格去写用例. 封装业务层的 DSL

目前已经实现了一部分, 还未充分测试. 已有的 dsl 已经很好的满足了需求. 所以设计这个需求并不大.

test("BDD样式") {
  用户 登录(username, telephone , password)
  券商 认证(tid, fund_account, account_password)
  券商 绑定()
  股票 选择 "乐视"
  股票 当前价格 100
  股票 当前价格
  股票 买入 100
  股票 选择 "中车"
  股票 卖出 200
  用户 注销
}

test("基于原有的api形式进行更高层的封装"){
  用户.登录.req.username=""
  用户.登录.req.password=""
  用户.登录.req.telephone=""
  用户.登录.res.result_code="60000"
}

支持使用思维导图设计用例

进行中

支持所有的结构化支持

html xml 等各种协议都可以支持. 可以考虑重用 diffy 的模块

更多计划

灵感

来自于百度内部的 SuperTest 框架. SuperTest 是行业接口测试框架的楷模, 我在百度时有幸维护过一段时间. 因为某些原因并没有开源出来.

httpapi 几乎是它的 jvm 简化实现版本

因为我们公司是业务型的公司, 所以没有太多的精力去做一个完美的框架. 我们组也无意去全力开发框架. 更想把精力投入在业务相关的事情上.

在业务接口监控, 用户体验分析, 移动端测试还有更大的事情要做. 所以计划开源出来给大家借鉴. 期待行业能有更好用的产品.

年前就会把工具开放到 github 上. 最近在逐渐的完善细节.

根据使用效果来看, 组内招聘的业务测试新人都是具备一定开发基础的. 都具备 java 和 eclipse 的使用经验. 来部门后被我们安利到了 IDEA 上. 我并没有培训他们 scala 语言. 他们到现在并不会 scala, 但是对这个框架运用的很熟练. 编写和补充测试用例非常快. 唯一吐槽的就是 sbt 的一些仓库被墙导致下载更新包容易卡住.
利用 jenkins 跑测试用例并生成报表. 帮我们发现了不少的接口问题. 定位问题非常的快. 对于公司新的项目, 后端和前端并行开发的情况下, 利用接口测试也做到了提前的质量保证, 保证了项目的平稳进展.

这个框架并不是特别简单, 在易用性和灵活性方面, 我选择了灵活性来保证金融领域复杂的业务可以得到充分的验证. 主要的设计者是我的主管@skytraveler和我, 我们也希望有更多人可以一起参与进来完善它. 后续会开源到 github 上 testerhome 组织里, 交给大家一起维护 .


↙↙↙阅读原文可查看相关链接,并与作者交流