TA的每日心情 | 擦汗 13 小时前 |
---|
签到天数: 1048 天 连续签到: 1 天 [LV.10]测试总司令
|
安装
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",
- }
- }
- }
- }
复制代码 执行如下命令测试接口。
其结果如下:
- 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 报告测试通过了。
原理
Apitest 执行测试文件时会加载全部测试用例,逐一执行,其执行过程可以描述为:根据 req 部分构造请求发送给服务器,收到响应后依据 res 校验响应数据,然后打印结果。
Apitest 中的用例文件格式是 JSONA。 JSONA是JSON的超集,减轻了一些JSON语法限制(不强制要求双引号,支持注释等),再添加了一个特性:注解。上面例子中的@partial就是注解。
为什么使用JSONA?
接口测试的本质的就是构造并发送req数据,接收并校验res数据。数据即是主体又是核心,而JSON是最可读最通用的数据描述格式。
接口测试还需要某些特定逻辑。比如请求中构造随机数,在响应中只校验给出的部分数据。
JSONA = JSON + Annotation(注解)。JSON负责数据部分,注解负责逻辑部分。完美的贴合接口测试需求。
特性
·跨平台
· DSL
- 类JSON,没有学习难度
- 编写简单,阅读容易
- 不要求编写者会编程
· 数据即断言
· 数据可访问
· 支持Mock
· 支持Mixin
· 支持CI
· 支持TDD
· 支持用户定义函数
· 跳过,延时,重试和循环
· 支持Form,文件上传,GraphQL
示例
全等校验
默认请求下,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,
- }
- }
- }
- }
复制代码 查询字符串
通过 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",
- }
- }
- },
- },
- }
复制代码 用例数据变量导出与引用
凡是执行过的用例其数据均可以当做已自动导出变量,它们均可以被后续用例引用。
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}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
- }
- }
- }
- }
复制代码
|
|