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.