跳至主要內容
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?

4563博客

全新的繁體中文 WordPress 網站
  • 首頁
  • Apitest 接口自动化测试工具
未分類
11 6 月 2021

Apitest 接口自动化测试工具

Apitest 接口自动化测试工具

資深大佬 : sigoden 4

推荐一款用类似 JSON 语法写接口测试的工具

特性

  • 跨平台
  • DSL
    • 类 JSON,没有学习难度
    • 编写简单,阅读容易
    • 不要求编写者会编程
  • 数据即断言
  • 数据可访问
  • 支持 Mock
  • 支持 Mixin
  • 支持 CI
  • 支持 TDD
  • 支持用户定义函数
  • 跳过,延时,重试和循环
  • 支持 Form,文件上传,GraphQL

安装

Apitest 工具是单可执行文件,不需要安装,放到PATH路径下面就可以直接运行

# linux curl -L -o apitest https://github.com/sigoden/apitest/releases/latest/download/apitest-linux  chmod +x apitest sudo mv apitest /usr/local/bin/  # macos curl -L -o apitest https://github.com/sigoden/apitest/releases/latest/download/apitest-macos chmod +x apitest sudo mv apitest /usr/local/bin/  # npm npm install -g @sigodenjs/apitest 

开始使用

编写测试文件 httpbin.jsona

{   test1: {     req: {       url: "https://httpbin.org/anything",       query: {         k1: "v1",       },     },     res: {       body: { @partial         args: {           "k1": "v2", // 注意,这儿应该是"v1", 我们故意写"v2"以测试 Apitest 的反应         },         url: "https://httpbin.org/anything?k1=v1",       }     }   } } 

执行如下命令测试接口

apitest httpbin.jsona 

其结果如下

main   test1 (2.554) ✘   main.test1.res.body.args.k1: v2 ≠ v1    {     "req": {       "url": "https://httpbin.org/anything",       "query": {         "k1": "v1"       }     },     "res": {       "headers": {         "date": "Thu, 17 Jun 2021 15:01:51 GMT",         "content-type": "application/json",         "content-length": "400",         "connection": "close",         "server": "gunicorn/19.9.0",         "access-control-allow-origin": "*",         "access-control-allow-credentials": "true"       },       "status": 200,       "body": {         "args": {           "k1": "v1"         },         "data": "",         "files": {},         "form": {},         "headers": {           "Accept": "application/json, text/plain, */*",           "Host": "httpbin.org",           "User-Agent": "axios/0.21.1",           "X-Amzn-Trace-Id": "Root=1-60cb63df-1b8592de3767882a6e865295"         },         "json": null,         "method": "GET",         "origin": "119.123.242.225",         "url": "https://httpbin.org/anything?k1=v1"       }     }   } 

Apitest 发现了 k1 的值异常 main.test1.res.body.args.k1: v2 ≠ v1 并打印错误,同时还打印了接口请求响应详情。

如果我们修改 main.test1.res.body.args.k1 值 v2 => v1 后再执行测试。

apitest httpbin.jsona 

其结果如下

main   test1 (1.889) ✔ 

Apitest 报告测试通过了。

原理

Apitest 执行测试文件时会加载全部测试用例,逐一执行,其执行过程可以描述为:根据 req 部分构造请求发送给服务器,收到响应后依据 res 校验响应数据,然后打印结果。

Apitest 中的用例文件格式是 JSONA。JSONA 是 JSON 的超集,减轻了一些 JSON 语法限制(不强制要求双引号,支持注释等),再添加了一个特性:注解。上面例子中的@partial就是注解。

为什么使用 JSONA ?

接口测试的本质的就是构造并发送req数据,接收并校验res数据。数据即是主体又是核心,而 JSON 是最可读最通用的数据描述格式。 接口测试还需要某些特定逻辑。比如请求中构造随机数,在响应中只校验给出的部分数据。

JSONA = JSON + Annotation(注解)。JSON 负责数据部分,注解负责逻辑部分。完美的贴合接口测试需求。

示例

下面的示例会用到一些注解,不明白的地方请查看README

全等校验

默认请求下,Apitest 进行全等校验。

  • 简单类型数据(null,boolean,string,number)完全相等
  • object 数据属性和属性值完全相等,字段顺序可以不一致
  • array 数据元素长度和各元素完全相等,元素顺序也要一致
{   test1: { @client("echo")     req: {       any: null,       bool: true,       str: "string",       int: 3,       float: 0.3,       obj: {a:3, b:4},       arr: [3,4],     },     res: {       any: null,       bool: true,       str: "string",       int: 3,       float: 0.3,       obj: {a:3, b:4},       // obj: {b:4, b:3}, object 类数据字段顺序可以不一致       arr: [3,4],     }   } } 

Apitest 保证:只有当实际接收到的 res 数据与我们用例中描述的 res 数据全等,测试才会通过。

数组校验技巧

Apitest 默认全等校验,而接口返回的 array 数据可能几十上百条,怎么办?

通常接口数据是结构化的,我们可以只校验数组第一个元素。

{   test1: { @client("echo")     req: {       arr: [         {name: "v1"},         {name: "v2"},         {name: "v3"},       ]     },     res: {       arr: [ @partial         {           name: "", @type         }       ],     }   } } 

如果 array 数据的长度也很关键呢?

{   test1: { @client("echo")     req: {       arr: [         {name: "v1"},         {name: "v2"},         {name: "v3"},       ]     },     res: {       arr: [ @every         [ @partial             {               name: "", @type             }         ],         `$.length === 3`, @eval       ],     }   } } 

对象校验技巧

Apitest 默认全等校验,而接口返回的 object 数据的属性很多,我们只关注其中部分属性?

{   test1: { @client("echo")     req: {       obj: {         a: 3,         b: 4,         c: 5,       }     },     res: {       obj: { @partial         b: 4,       }     }   } } 

接口可能返回一些可选字段,我们使用@optional标记这种字段

{   test1: { @client("echo")     req: {       v1: 3,       // v2: 4, 可选字段     },     res: {       v1: 3,       v2: 4, @optional     }   } } 

查询字符串

通过 req.query 传入 QueryString

{   test1: {     req: {       url: "https://httpbin.org/get",       query: {         k1: "v1",         k2: "v2",       }     },     res: {       body: { @partial         url: "https://httpbin.org/get?k1=v1&k2=v2",       }     }   } } 

当然你可以把 QueryString 直接写在req.url中

{   test1: {     req: {       url: "https://httpbin.org/get?k1=v1&k2=v2",     },     res: {       body: { @partial         url: "https://httpbin.org/get?k1=v1&k2=v2",       }     }   } } 

路径变量

通过 req.params 传入路径变量

{   test1: {     req: {       url: "https://httpbin.org/anything/{id}",       params: {         id: 3,       }     },     res: {       body: { @partial         url: "https://httpbin.org/anything/3"       }     }   } } 

请求头 /响应头

通过 req.headers 传入请求头,通过 res.headers 校验响应头

{   setCookies: { @describe("response with set-cookies header")     req: {       url: "https://httpbin.org/cookies/set",       query: {         k1: "v1",         k2: "v2",       },     },     res: {       status: 302,       headers: { @partial         'set-cookie': [           "k1=v1; Path=/",           "k2=v2; Path=/",         ],        },       body: "", @type     }   },   useCookies: { @describe("request with cookie header")     req: {       url: "https://httpbin.org/cookies",       headers: {         Cookie: `setCookies.res.headers["set-cookie"]`, @eval       }     },     res: {       body: { @partial         cookies: {           k1: "v1",           k2: "v2",         }       }     },   }, } 

校验 http 状态码

{   test1: {     req: {       url: "https://httpbin.org/status/401",     },     res: {       status: 401,     }   } } 

用例数据变量导出与引用

凡是执行过的用例其数据均可以当做已自动导出变量,它们均可以被后续用例引用。

Apitest 中可以使用 @eval 注解引用用例数据。

比如上面例子中setCookies.res.headers["set-cookie"],就是引用前面setCookies用例的set-cookie响应头数据。

表单: x-www-form-urlencoded

{   test1: { @describe('test form')     req: {       url: "https://httpbin.org/post",       method: "post",       headers: {         'content-type':"application/x-www-form-urlencoded"       },       body: {         v1: "bar1",         v2: "Bar2",       }     },     res: {       status: 200,       body: { @partial         form: {           v1: "bar1",           v2: "Bar2",         }       }     }   }, } 

表单: multipart/form-data

结合 @file 注解实现文件上传

{   test1: { @describe('test multi-part')     req: {       url: "https://httpbin.org/post",       method: "post",       headers: {         'content-type': "multipart/form-data",       },       body: {         v1: "bar1",         v2: "httpbin.jsona", @file       }     },     res: {       status: 200,       body: { @partial         form: {           v1: "bar1",           v2: "", @type         }       }     }   } } 

GraphQL

{   test1: { @describe("test graphql")     req: {       url: "https://api.spacex.land/graphql/",       body: {         query: ``query {   launchesPast(limit: ${othertest.req.body.count}) {     mission_name     launch_date_local     launch_site {       site_name_long     }   } }`` @eval       }     },     res: {       body: {         data: {           launchesPast: [ @partial             {               "mission_name": "", @type               "launch_date_local": "", @type               "launch_site": {                 "site_name_long": "", @type               }             }           ]         }       }     }   } } 

http(s)代理

{   @client({     name: "default",     type: "http",     options: {       proxy: "http://localhost:8080",     }   })   test1: {     req: {       url: "https://httpbin.org/ip",     },     res: {       body: {         origin: "", @type       }     }   } } 

Apitest 支持通过 HTTP_PROXY HTTPS_PROXY 环境变量开全局代理

多个接口服务地址

{   @client({     name: "api1",     type: "http",     options: {       baseURL: "http://localhost:3000/api/v1",     }   })   @client({     name: "api2",     type: "http",     options: {       baseURL: "http://localhost:3000/api/v2",     }   })   test1: { @client("api1")     req: {       url: "/signup", // => http://localhost:3000/api/v1/signup     }   },   test2: { @client("api2")     req: {       url: "/signup", // => http://localhost:3000/api/v2/signup     }   } }  

自定义超时

你可以设置客户端超时,影响所有使用该客户端的接口

{   @client({     name: "default",     type: "http",     options: {       timeout: 30000,      }   }) } 

你也可以为某个用例设置超时

{   test1: { @client({options:{timeout: 30000}})    } } 

环境变量传递数据

{   test1: {     req: {       headers: {         "x-key": "env.API_KEY", @eval       }     }   } } 

mock 数据

{   login1: {     req: {       url: "/signup",       body: {         username: 'username(3)', @mock         password: 'string(12)', @mock         email: `req.username + "@gmail.com"`, @eval       }     }   } } 

Apitest 支持近 40 个 mock 函数。下面列些常用的

{   test1: {     req: {       email: 'email', @mock       username: 'username', @mock       integer: 'integer(-5, 5)', @mock       image: 'image("200x100")', @mock       string: 'string("alpha", 5)', @mock       date: 'date', @mock  // iso8601 格式的当前时间 // 2021-06-03T07:35:55Z       date2: 'date("","2 weeks ago")', @mock // 2 周前       sentence: 'sentence', @mock       cnsentence: 'cnsentence', @mock // 中文段落         }   } } 

用例组

{   @describe("这是一个模块")   @client({name:"default",kind:"echo"})   group1: { @group @describe("这是一个组")     test1: { @describe("最内用例")       req: {       }     },     group2: { @group @describe("这是一个嵌套组")       test1: { @describe("嵌套组内的用例")         req: {         }       }     }   } } 

上面的测试文件打印如下

这是一个模块   这是一个组     最内用例 ✔     这是一个嵌套组       嵌套组内的用例 ✔ 

跳过用例(组)

{   test1: { @client("echo")     req: {     },     run: {       skip: `othertest.res.status === 200`, @eval     }   } } 

延时执行用例(组)

{   test1: { @client("echo")     req: {     },     run: {       delay: 1000, // 延时毫秒     }   } } 

重试用例(组)

{   test1: { @client("echo")     req: {     },     run: {       retry: {         stop:'$run.count> 2', @eval // 终止重试条件         delay: 1000, // 重试间隔毫秒       }     },   } } 

重复执行用例(组)

{   test1: { @client("echo")     req: {       v1:'$run.index', @eval       v2:'$run.item', @eval     },     run: {       loop: {         delay: 1000, // 重复执行间隔毫秒         items: [  // 重复执行数据           'a',           'b',           'c',         ]       }     },   } } 

如果不在意数据,只想重复执行多少次的话,可以这样设置

{   test1: {     run: {       delay: 1000,       items: `Array(5)`, @eval     }   } } 

强制打印详情

常规模式下,接口如果没有出错是不会打印数据详情的。通过设置run.dump为 true 强制打印详情数据。

{   test1: { @client("echo")     req: {     },     run: {       dump: true,     }   } } 

抽离公用逻辑以复用

首先创建一个文件存储 Mixin 定义的文件

// mixin.jsona {   createPost: { // 抽离路由信息到 mixin     req: {       url: '/posts',       method: 'post',     },   },   auth1: { // 抽离鉴权到 minxin     req: {       headers: {         authorization: `"Bearer " + test1.res.body.token`, @eval       }     }   } } 
@mixin("mixin") // 引入 mixin.jsona 文件  {   createPost1: { @describe("写文章 1") @mixin(["createPost", "auth1"])     req: {       body: {         title: "sentence", @mock       }     }   },   createPost2: { @describe("写文章 2,带描述") @mixin(["createPost", "auth1"])     req: {       body: {         title: "sentence", @mock         description: "paragraph", @mock       }     }   }, } 

越是频繁用到的数据越适合抽离到 Mixin 。 

自定义函数

某些情况下,Apitest 内置的注解不够用,你可以使用自定义函数。

编写函数lib.js

 // 创建随机颜色 exports.makeColor = function () {   const letters = "0123456789ABCDEF";   let color = "#";   for (let i = 0; i < 6; i++) {     color += letters[Math.floor(Math.random() * 16)];   }   return color; }  // 判断是否是 ISO8601(2021-06-02:00:00.000Z)风格的时间字符串 exports.isDate = function (date) {   return /^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}.d{3}Z$/.test(date) } 

使用函数

@jslib("lib") // 引入 js 文件  {   test1: {     req: {       body: {         color: 'makeColor()', @eval // 调用 `makeColor` 函数生成随机颜色       }     },     res: {       body: {         createdAt: 'isDate($)', @eval // $ 表示须校验字段,对应响应数据`res.body.createdAt`          // 当然你可以直接使用 regex         updatedAt: `/^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}.d{3}Z$/.test($)`, @eval       }     }   } } 

后记

这里列举了一下 Apitest 使用示例,详细说明请点击github.com/sigoden/apitest查看。

大佬有話說 (3)

  • 資深大佬 : zagfai

    直接写点 python 不行?

  • 主 資深大佬 : sigoden

    – 大部分人都不会 Python 。
    – Jsona 的容易写,可读性也更强

  • 資深大佬 : encro

    鼓励,
    不过变量替换也是一个比较重要的特性,
    比如 URL,TOKEN 可能依赖全局定义或者上一个请求。

文章導覽

上一篇文章
下一篇文章

AD

其他操作

  • 登入
  • 訂閱網站內容的資訊提供
  • 訂閱留言的資訊提供
  • WordPress.org 台灣繁體中文

51la

4563博客

全新的繁體中文 WordPress 網站
返回頂端
本站採用 WordPress 建置 | 佈景主題採用 GretaThemes 所設計的 Memory
4563博客
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?
在這裡新增小工具