The installation
The Apitest tool is a single executable file that does not need to be installed and can be run directly by placing it under the 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
Copy the code
Begin to use
Write the test file httpbin.jsona
{ test1: { req: { url: "https://httpbin.org/anything", query: { k1: "v1", }, }, res: { body: { @partial args: { "k1": "V2", / / note that here should be "v1", deliberately we write "v2" to test the response of the Apitest}, url: "https://httpbin.org/anything?k1=v1",}}}}Copy the code
Run the following command to test the interface
apitest httpbin.jsona
Copy the code
The results are as follows
The main test1 (2.554) ✘ main. Test1. Res. Body. The args. K1: v2 indicates v1 {" the 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, * / *", "the Host" : "httpbin.org", "the 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" } } }Copy the code
Apitest found abnormal value. The main of k1 test1. Res. Body. The args. K1: v2 indicates v1 and typing errors, also has a print interface details request and response.
If we change the main test1. Res. Body. The args. The k1 value v2 = > v1 and then execute the test.
apitest httpbin.jsona
Copy the code
The results are as follows
The main test1 ✔ (1.889)Copy the code
Apitest reports that the test passed.
The principle of
Apitest will load all the test cases when executing the test file, and execute them one by one. The execution process can be described as follows: send the request to the server according to the partial construction of REQ, verify the response data according to RES after receiving the response, and then print the result.
The use-case file format in Apitest is JSONA. JSONA is a JSON superset that relieves some of the constraints of JSON syntax (no mandatory double quotes, support for comments, etc.) and adds one more feature: annotations. The @partial in the above example is an annotation.
Why use JSONA?
The essence of interface testing is to construct and send REQ data and to receive and validate RES data. Data is both the body and the heart, and JSON is the most readable and versatile data description format. Interface testing also requires some specific logic. For example, construct random numbers in the request and verify only part of the data given in the response.
JSONA = JSON + Annotation. JSON takes care of the data, annotations take care of the logic. Perfect fit for interface testing requirements.
features
- cross-platform
- DSL
- Json-like, easy to learn
- Simple to write and easy to read
- Programmers are not required to be able to program
- Data as assertion
- Data accessibility
- Support the Mock
- Support a Mixin
- Support for CI
- Support for TDD
- Support for user-defined functions
- Skip, delay, retry and loop
- Support Form, file upload,GraphQL
The sample
The following example uses some annotations. If you don’t understand, check out README
Congruent check
By default, Apitest performs parity check.
- Simple data types (null, Boolean, string, number) completely equal
- Object Data attributes and attribute values are exactly the same, and the order of the fields can be different
- The length of the array data elements must be exactly the same as each element, and the order of the elements must be the same
{
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 data field order can be inconsistent
arr: [3.4].}}}Copy the code
Apitest guarantees that the test will only pass if the RES data actually received is identical to the RES data described in our use case.
Array check technique
Apitest default parity check, and the interface may return dozens or hundreds of array data, how to do?
Usually the interface data is structured and we can only validate the first element of the array.
{
test1: { @client("echo")
req: {
arr: [{name: "v1"},
{name: "v2"},
{name: "v3"}},],res: {
arr: [ @partial
{
name: "", @type
}
],
}
}
}
Copy the code
What if the length of array data is also critical?
{
test1: { @client("echo")
req: {
arr: [{name: "v1"},
{name: "v2"},
{name: "v3"}},],res: {
arr: [ @every
[ @partial
{
name: "", @type
}
],
`$.length === 3`The @eval].}}}Copy the code
Object Verification Techniques
Apitest default universal check, and the interface returned object data many attributes, we only focus on some of the attributes?
{
test1: { @client("echo")
req: {
obj: {
a: 3.b: 4.c: 5,}},res: {
obj: { @partial
b: 4,}}}}Copy the code
Query string
QueryString is passed through req.query
{
test1: {
req: {
url: "https://httpbin.org/get".query: {
k1: "v1".k2: "v2",}},res: {
body: { @partial
url: "https://httpbin.org/get?k1=v1&k2=v2",}}}}Copy the code
Of course you can write QueryString directly in the 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",
}
}
}
}
Copy the code
Path variable
Pass the path variable through req.params
{
test1: {
req: {
url: "https://httpbin.org/anything/{id}".params: {
id: 3,}},res: {
body: { @partial
url: "https://httpbin.org/anything/3"}}}}Copy the code
Request header/response header
The request header is passed through req.headers and the response header is verified by 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"]`The @eval}},res: {
body: { @partial
cookies: {
k1: "v1".k2: "v2",}}},},}Copy the code
Use case data variables export and reference
The data of all executed use cases can be treated as automatically exported variables and can be referenced by subsequent use cases.
Use case data can be referenced in Apitest using the @eval annotation.
For example, setcookie.res. headers[“set-cookie”] in the above example is the set-cookie response header data that references the previous setCookies use case.
Form: the 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",}}}},}Copy the code
Form: the multipart/form – the data
File upload with @file annotation
{
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
}
}
}
}
}
Copy the code
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
}
}
]
}
}
}
}
}
Copy the code
HTTP (s) agent
{
@client({
name: "default".type: "http".options: {
proxy: "http://localhost:8080",}})test1: {
req: {
url: "https://httpbin.org/ip",},res: {
body: {
origin: "", @type
}
}
}
}
Copy the code
Apitest supports global proxy using the HTTP_PROXY HTTPS_PROXY environment variable
Multiple interface service addresses
{
@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}}}Copy the code
Custom timeout
You can set the client timeout to affect all interfaces that use the client
{
@client({
name: "default".type: "http".options: {
timeout: 30000,}})}Copy the code
You can also set a timeout for a use case
{
test1: { @client({options: {timeout: 30000}}}})Copy the code
Environment variables pass data
{
test1: {
req: {
headers: {
"x-key": "env.API_KEY"The @eval}}}}Copy the code
The mock data
{
login1: {
req: {
url: "/signup".body: {
username: 'username(3)', @mock
password: 'string(12)', @mock
email: `req.username + "@gmail.com"`The @eval}}}}Copy the code
Apitest supports nearly 40 mock functions. Here are some common ones
{
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 // Current time in ISO8601 format // 2021-06-03:35:55z
date2: 'date("","2 weeks ago")', @mock / / 2 weeks ago
sentence: 'sentence', @mock
cnsentence: 'cnsentence', @mock // Chinese paragraph}}}Copy the code
Use case group
{
@describe("This is a module.")
@client({name:"default".kind:"echo"})
group1: { @group @describe("This is a group.")
test1: { @describe(Innermost use Case)
req: {}},group2: { @group @describe("This is a nested group")
test1: { @describe("Use cases within nested groups")
req: {}}}}}Copy the code
The above test file is printed below
➤ Here's a module. Here's a group most internal use case. Here's a nested groupCopy the code
Skipping use cases (groups)
{
test1: { @client("echo")
req: {},run: {
skip: `othertest.res.status === 200`The @eval}}}Copy the code
Delayed execution use Cases (Groups)
{
test1: { @client("echo")
req: {},run: {
delay: 1000.// Delay milliseconds}}}Copy the code
Retry Example (Group)
{
test1: { @client("echo")
req: {},run: {
retry: {
stop:'$run.count> 2'The @eval // Terminates the retry condition
delay: 1000.// Retry interval in milliseconds}}}},Copy the code
Repeat execution of use Cases (groups)
{
test1: { @client("echo")
req: {
v1:'$run.index'The @eval
v2:'$run.item'The @eval
},
run: {
loop: {
delay: 1000.// Repeat execution interval is milliseconds
items: [ // Execute data repeatedly
'a'.'b'.'c',]}},}}Copy the code
If you don’t care about the data and just want to repeat it how many times, you can do this
{
test1: {
run: {
delay: 1000.items: `Array(5)`The @eval}}}Copy the code
Forcing print details
In normal mode, the interface does not print data details without an error. Run. Dump is set to true to force details to be printed.
{
test1: { @client("echo")
req: {},run: {
dump: true,}}}Copy the code
Extract common logic for reuse
Start by creating a file to store the Mixin definition
// mixin.jsona
{
createPost: { // Extract routing information to mixin
req: {
url: '/posts'.method: 'post',}},auth1: { // Remove authentication to minxin
req: {
headers: {
authorization: `"Bearer " + test1.res.body.token`The @eval}}}}Copy the code
@mixin("mixin") // Import the mixin.jsona file
{
createPost1: { @describe("Write essay 1") @mixin(["createPost"."auth1"])
req: {
body: {
title: "sentence", @mock
}
}
},
createPost2: { @describe("Write essay 2, with description.") @mixin(["createPost"."auth1"])
req: {
body: {
title: "sentence", @mock
description: "paragraph", @mock
}
}
},
}
Copy the code
The more frequently you use the data, the better it is to pull it out of the Mixin.
Custom function
In some cases, Apitest’s built-in annotations are not enough, so you can use custom functions instead.
Write the function lib.js
// Create random colors
exports.makeColor = function () {
const letters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
// Check whether it is an ISO8601(2021-06-02:00:00.000z) style time string
exports.isDate = function (date) {
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(date)
}
Copy the code
Using the function
@jslib("lib") // Import the js file
{
test1: {
req: {
body: {
color: 'makeColor()'The @eval // Call 'makeColor' to generate random colors}},res: {
body: {
createdAt: 'isDate($)'The @eval // $indicates the field to be verified, corresponding to the response data 'res.body.createdat'
// You can use regex directly
updatedAt: `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test($)`The @eval}}}}Copy the code
Afterword.
Here are some examples of how to use Apitest. For more information, visit github.com/sigoden/api… Look at it.