推荐一款用类似 JSON 语法写接口测试的工具
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 进行全等校验。
{
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",
}
}
},
},
}
{
test1: {
req: {
url: "https://httpbin.org/status/401",
},
res: {
status: 401,
}
}
}
凡是执行过的用例其数据均可以当做已自动导出变量,它们均可以被后续用例引用。
Apitest 中可以使用 @eval
注解引用用例数据。
比如上面例子中setCookies.res.headers["set-cookie"]
,就是引用前面setCookies
用例的set-cookie
响应头数据。
{
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",
}
}
}
},
}
结合 @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
}
}
}
}
}
{
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
}
}
]
}
}
}
}
}
{
@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
}
}
}
}
{
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}T\d{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}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test($)`, @eval
}
}
}
}
这里列举了一下 Apitest 使用示例,详细说明请点击github.com/sigoden/apitest查看。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.