{"id":16278587,"url":"https://github.com/sigoden/apitest","last_synced_at":"2025-06-25T09:33:57.869Z","repository":{"id":50048801,"uuid":"372659219","full_name":"sigoden/apitest","owner":"sigoden","description":"Apitest is declarative api testing tool with JSON-like DSL.","archived":false,"fork":false,"pushed_at":"2022-07-06T03:34:06.000Z","size":408,"stargazers_count":112,"open_issues_count":1,"forks_count":10,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-05-25T08:41:48.044Z","etag":null,"topics":["api-testing","ci","jsona","tdd","test-automation","testing-tool","testrunner"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sigoden.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-06-01T00:27:17.000Z","updated_at":"2025-03-02T01:38:14.000Z","dependencies_parsed_at":"2022-09-02T00:42:30.712Z","dependency_job_id":null,"html_url":"https://github.com/sigoden/apitest","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/sigoden/apitest","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sigoden%2Fapitest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sigoden%2Fapitest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sigoden%2Fapitest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sigoden%2Fapitest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sigoden","download_url":"https://codeload.github.com/sigoden/apitest/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sigoden%2Fapitest/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261844813,"owners_count":23218440,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-testing","ci","jsona","tdd","test-automation","testing-tool","testrunner"],"created_at":"2024-10-10T18:59:05.222Z","updated_at":"2025-06-25T09:33:57.842Z","avatar_url":"https://github.com/sigoden.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Apitest\n\n[![build](https://github.com/sigoden/apitest/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/sigoden/apitest/actions/workflows/ci.yaml)\n[![release](https://img.shields.io/github/v/release/sigoden/apitest)](https://github.com/sigoden/apitest/releases)\n[![npm](https://img.shields.io/npm/v/@sigodenjs/apitest)](https://www.npmjs.com/package/@sigodenjs/apitest)\n\nApitest is declarative api testing tool with JSON-like DSL.\n\nRead this in other languages: [中文](./README.zh-CN.md)\n\n- [Apitest](#apitest)\n  - [Installation](#installation)\n  - [Get Started](#get-started)\n  - [Features](#features)\n    - [JSONA DSL](#jsona-dsl)\n    - [Data Is Assertion](#data-is-assertion)\n    - [Data Is Accessable](#data-is-accessable)\n    - [Support Mock](#support-mock)\n    - [Support Mixin](#support-mixin)\n    - [CI Support](#ci-support)\n    - [TDD Support](#tdd-support)\n    - [User-defiend Functions](#user-defiend-functions)\n    - [Skip, Delay, Retry \u0026 Loop](#skip-delay-retry--loop)\n    - [Form, File Upload, GraphQL](#form-file-upload-graphql)\n  - [Annotation](#annotation)\n    - [@module](#module)\n    - [@jslib](#jslib)\n    - [@mixin](#mixin)\n    - [@client](#client)\n    - [@describe](#describe)\n    - [@group](#group)\n    - [@eval](#eval)\n    - [@mock](#mock)\n    - [@file](#file)\n    - [@trans](#trans)\n    - [@every](#every)\n    - [@some](#some)\n    - [@partial](#partial)\n    - [@type](#type)\n    - [@optional](#optional)\n    - [@nullable](#nullable)\n  - [Run](#run)\n    - [Skip](#skip)\n    - [Delay](#delay)\n    - [Retry](#retry)\n    - [Loop](#loop)\n    - [Dump](#dump)\n  - [Client](#client-1)\n    - [Echo](#echo)\n    - [Http](#http)\n      - [Options](#options)\n      - [Cookies](#cookies)\n      - [x-www-form-urlencoded](#x-www-form-urlencoded)\n      - [multipart/form-data](#multipartform-data)\n      - [graphql](#graphql)\n  - [Cli](#cli)\n    - [Multiple Test Environments](#multiple-test-environments)\n    - [Normal Mode](#normal-mode)\n    - [CI Mode](#ci-mode)\n\n## Installation\n\nBinaries are available in [Github Releases](https://github.com/sigoden/apitest/releases). Make sure to put the path to the binary into your `PATH`.\n\n```\n# linux\ncurl -L -o apitest https://github.com/sigoden/apitest/releases/latest/download/apitest-linux \nchmod +x apitest\nsudo mv apitest /usr/local/bin/\n\n# macos\ncurl -L -o apitest https://github.com/sigoden/apitest/releases/latest/download/apitest-macos\nchmod +x apitest\nsudo mv apitest /usr/local/bin/\n\n# npm\nnpm install -g @sigodenjs/apitest\n```\n\n## Get Started\n\nWrite test file `httpbin.jsona`\n\n```\n{\n  test1: {\n    req: {\n      url: \"https://httpbin.org/post\",\n      method: \"post\",\n      headers: {\n        'content-type':'application/json',\n      },\n      body: {\n        v1: \"bar1\",\n        v2: \"Bar2\",\n      },\n    },\n    res: {\n      status: 200,\n      body: { @partial\n        json: {\n          v1: \"bar1\",\n          v2: \"bar2\"\n        }\n      }\n    }\n  }\n}\n\n```\n\nRun test\n\n```\napitest httpbin.jsona\n\nmain\n   test1 (0.944) ✘\n   main.test1.res.body.json.v2: bar2 ≠ Bar2\n\n   ...\n```\n\nThe use case test failed. From the error message printed by Apitest, you can see that the actual value of `main.test1.res.body.json.v2` is `Bar2` instead of `bar2`.\n\nAfter we modify `bar2` to `Bar2`, execute Apitest again\n\n```\napitest httpbin.jsona\n\nmain\n   test1 (0.930) ✔\n```\n\n## Features\n\n### JSONA DSL\n\nUse JSON-like DSL to write tests. The document is the test.\n\n```\n{\n  test1: { @describe(\"user login\")\n    req: {\n      url: 'http://localhost:3000/login'\n      method: 'post',\n      body: {\n        user: 'jason',\n        pass: 'a123456,\n      }\n    },\n    res: {\n      status: 200\n      body: {\n        user: 'jason',\n        token: '', @type\n        expireIn: 0, @type\n      }\n    }\n  }\n}\n```\n\nAccording to the above use case, I don't need to elaborate, an experienced backend should be able to guess what parameters are passed by this api and what data is returned by the server.\n\nThe working principle of Apitest is to construct the request according to the description in the `req` part and send it to the backend. After receiving the response data from the backend, verify the data according to the description in the `res` part.\n\nPlease don't be scared by DSL. In fact, it is JSON, which loosens some grammatical restrictions (double quotes are not mandatory, comments are supported, etc.), and only one feature is added: comments. In the above example, `@describe`, `@type` is [Annotation](#Annotation).\n\nClick [jsona/spec](https://github.com/jsona/spec) to view the JSONA specification\n\n\u003e By the way, there is a vscode extension supports DSL (jsona) format.\n\nWhy use JSONA?\n\nThe essence of api testing is to construct and send `req` data, and receive and verify `res` data. Data is both the main body and the core, and JSON is the most readable and universal data description format.\nApi testing also requires some specific logic. For example, a random number is constructed in the request, and only part of the data given in the response is checked.\n\nJSONA = JSON + Annotation. JSON is responsible for the data part, and annotations are responsible for the logic part. Perfectly fit the interface test requirements.\n\n### Data Is Assertion\n\nHow to understand? See below.\n\n```json\n{\n  \"foo1\": 3,\n  \"foo2\": [\"a\", \"b\"],\n  \"foo3\": {\n    \"a\": 3,\n    \"b\": 4\n  }\n}\n```\n\nAssuming that the response data is as above, the test case is as follows:\n\n```\n{\n  test1: {\n    req: {\n    },\n    res: {\n      body: {\n        \"foo1\": 3,\n        \"foo2\": [\"a\", \"b\"],\n        \"foo3\": {\n          \"a\": 3,\n          \"b\": 4\n        }\n      }\n    }\n  }\n}\n```\n\nThat's right, it's exactly the same. Apitest will compare each part of the data one by one. Any inconsistency will cause the test to fail.\n\nThe strategy provided by conventional testing tools is addition. This is very important and I just add an assertion. In Apitest, you can only do subtraction. This data is not concerned. I actively ignore or relax the verification.\n\nFor example, the previous test case\n\n```\n{\n  test1: { @describe(\"user login\")\n    ...\n    res: {\n      body: {\n        user: 'jason',\n        token: '', @type\n        expireIn: 0, @type\n      }\n    }\n  }\n}\n```\n\nWe still checked all the fields. Because the values of `token` and `expireIn` are changed, we use `@type` to tell Apitest to only check the type of the field and ignore the specific value.\n\n### Data Is Accessable\n\nAny data of the test case can be testd by subsequent test cases\n\nThe following test cases can use all the data of the previous test cases.\n\n```\n{\n  test1: { @describe(\"user login\")\n    ...\n    res: {\n      body: {\n        token: '', @type\n      }\n    }\n  },\n  test2: { @describe(\"create article\")\n    req: {\n      headers: {\n        // We access the response data of the previous test case test1.\n        authorization: `\"Bearer \" + test1.res.body.token`, @eval\n      },\n    }\n  },\n}\n```\n\n### Support Mock\n\nWith Mock, no longer entangled in fabricating data, Seee [@mock](#mock)\n\n\n### Support Mixin \n\nUse Mixin skillfully, get rid of copy and paste. See [@mixin](#mixin)\n\n\n### CI Support\n\nAs a command line tool itself, it is very easy to integrate with the back-end ci. And apitest also provides the `--ci` option to optimize ci.\n\n### TDD Support\n\nYou can even write only the `req` part, and after the api has a response, paste the response data directly as the `res` part. Talk of experience 🐶\n\nIn the default mode (not ci), when Apitest encounters a failed test, it will print an error and exit. Apitest has cached test data. You can repeatedly execute wrong test cases, develop and test at the same time, and then enter the follow-up test until you get through.\n\nAt the same time, you can also select a test case to execute through the `--only` option.\n\n### User-defiend Functions\n\nYou don't need to use this function at all. But I still worry about the need in certain extreme or corner scenes, so I still support it.\n\nApitest allows users to write custom functions through js to construct request data or verify response data. (Dare to call it a cross-programming language? 🐶), See [@jslib](#jslib)\n\n### Skip, Delay, Retry \u0026 Loop\n\nSee [#Run](#run)\n\n### Form, File Upload, GraphQL\nSee [#Http](#http)\n\n## Annotation\n\nApitest uses JSONA format to describe test cases.\n\nJSON describes data and annotation describes logic.\n\n### @module\n\n**Import submodule**\n\u003e scope: entrypoint file\n\n```\n// main.jsona\n{\n  @module(\"mod1\")\n}\n\n// mod1.jsona\n{\n  test1: {\n    req: {\n    }\n  }\n}\n```\n\n### @jslib\n\n**Import user-defined functions**\n\u003e scope: entrypoint file\n\nWrite functions `lib.js`\n\n```js\n// Make random color e.g. #34FFFF\nexports.makeColor = function () {\n  const letters = '0123456789ABCDEF';\n  let color = '#';\n  for (let i = 0; i \u003c 6; i++) {\n    color += letters[Math.floor(Math.random() * 16)];\n  }\n  return color;\n}\n\n// Detect date in ISO8601(e.g. 2021-06-02:00:00.000Z) format\nexports.isDate = function (date) {\n  return /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/.test(date)\n}\n```\n\nUse functions\n\n```\n@jslib(\"lib\") // Import js files\n\n{\n   test1: {\n     req: {\n       body: {\n        // call the `makeColor` function to generate random colors\n        color:'makeColor()', @eval \n       }\n     },\n     res: {\n       body: {\n        // $ indicates the field to be verified, here is `res.body.createdAt`\n        createdAt:'isDate($)', @eval\n\n        // Of course you can use regex directly\n        updatedAt: `/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/ .test($)`, @eval\n       }\n     }\n   }\n}\n```\n\n### @mixin\n\n**Import mixin file**\n\u003e scope: entrypoint file, group/unit head\n\nFirst create a file to store the file defined by Mixin\n```\n{\n  createPost: { // Extract routing information to mixin\n    req: {\n      url: '/posts',\n      method: 'post',\n    },\n  },\n  auth1: { // Extract authorization\n    req: {\n      headers: {\n        authorization: `\"Bearer \" + test1.res.body.token`, @eval\n      }\n    }\n  }\n}\n```\n\n```\n@mixin(\"mixin\") // include mixin.jsona\n{\n    createPost1: { @describe(\"create article 1\") @mixin([\"createPost\", \"auth1\"])\n        req: {\n            body: {\n                title: \"sentence\", @mock\n            }\n        }\n    },\n    createPost2: { @describe(\"create article 2，with description\") @mixin([\"createPost\", \"auth1\"])\n        req: {\n            body: {\n                title: \"sentence\", @mock\n                description: \"paragraph\", @mock\n            }\n        }\n    },\n}\n```\n\nThe more frequently used part, the more suitable it is to be extracted to Mixin.\n\n### @client\n\n\n**Setup clients**\n\u003e scope: entrypoint file, group/unit head\n\n[Client](#client) is responsible for constructing a request according to `req`, sending it to the server, receiving the response from the server, and constructing `res` response data.\n\n```\n{\n  @client({\n    name: \"apiv1\",\n    kind: \"http\",\n    options: {\n      baseURL: \"http://localhost:3000/api/v1\",\n      timeout: 30000,\n    }\n  })\n  @client({\n    name: \"apiv2\",\n    kind: \"http\",\n    options: {\n      baseURL: \"http://localhost:3000/api/v2\",\n      timeout: 30000,\n    }\n  })\n  test1: { @client(\"apiv1\") \n    req: {\n      url: \"/posts\" // 使用apiv1客户端，所以请求路径是  http://localhost:3000/api/v1/posts\n    }\n  },\n  test2: { @client({name:\"apiv2\",options:{timeout:30000}})\n    req: {\n      url: \"/key\" // 使用apiv2客户端，所以请求路径是 http://localhost:3000/api/v2/posts\n    }\n  },\n}\n```\n\n### @describe\n\n\n**Give a title**\n\u003e scope: module file, group/unit head\n\n```\n{\n  @client({name:\"default\",kind:\"echo\"})\n  @describe(\"This is a module\")\n  group1: { @group @describe(\"This is a group\")\n    test1: { @describe(\"A unit in group\")\n      req: {\n      }\n    },\n    group2: { @group @describe(\"This is a nested group\")\n      test1: { @describe(\"A unit in nested group\")\n        req: {\n        }\n      }\n    }\n  }\n}\n```\n\nIt will be printed as follows\n\n```\nThis is a module\n  This is a group\n    A unit in group ✔\n    This is a nested group\n      A unit in nested group ✔\n```\n\nIf the `@description` is removed, it will be printed as follows\n\n```\nmain\n  group1\n    test1 ✔\n    group2\n      test1 ✔\n```\n\n### @group\n\n**Mark as case group**\n\u003e scope: group head\n\nThe test cases in the group will inherit the group's `@client` and `@mixin`. The group also supports [Run](#run).\n\n\n```\n{\n  group1: { @group @mixin(\"auth1\") @client(\"apiv1\")\n    test1: {\n\n    },\n    // The mixin of the use case and the mixin of the group will be merged into @mixin([\"route1\",\"auth1\"])\n    test2: { @mixin(\"route1\") \n\n    },\n    // The client of the use case will overwrite the client of the group\n    test3: { @client(\"echo\") \n\n    },\n    group2: { @group // nest group\n\n    },\n    run: {\n\n    }\n  }\n}\n```\n\n### @eval\n\n**Use js expr to generate data (in `req`) and verify data(in `res`)**\n\u003e scope: unit block\n\n`@eval` features:\n\n- can use js builtin functions\n- can use jslib functions\n- can access environment variables\n- can use the data from the previous test\n\n```\n{\n  test1: { @client(\"echo\")\n    req: {\n      v1: \"JSON.stringify({a:3,b:4})\", @eval // Use JS built-in functions\n      v2: `\n        let x = 3;\n        let y = 4;\n        x + y\n        `, @eval  // Support code block\n      v3: \"env.FOO\", @eval // Access environment variables\n      v4: 'mod1.test1.res.body.id`, @eval // Access the data of the previous test\n    }\n  }\n}\n\n```\n\n`@eval` in `res` part with additional features:\n\n- `$` repersent response data in the position\n- return value true means that the verification passed\n- if the return value is not of type bool, the return value and the response data will be checked for congruent matching\n\n```\n{\n  rest2: {\n    res: {\n      v1: \"JSON.parse($).a === 3\",  @eval // $ is `res.v1`\n      v2: \"true\", @eval // true force test passed\n      v4: 'mod1.test1.res.body.id`, @eval // return value congruent matching\n    }\n  }\n}\n```\n\n**`@eval` accessing use case data with elision**\n\n```\n{\n  test1: {\n    req: {\n      v1: 3,\n    },\n    res: {\n      v1: \"main.test1.req.v1\", @eval\n   // v1:      \"test1.req.v1\", @eval\n   // v1:            \"req.v1\", @eval\n    }\n  }\n}\n```\n\n### @mock\n\n**Use mock function to generate data**\n\u003e scope: unit req block\n\nApitest supports nearly 40 mock functions. For a detailed list, see [fake-js](https://github.com/sigoden/fake-js#doc)\n\n```\n{\n  test1: {\n    req: {\n      email: 'email', @mock\n      username: 'username', @mock\n      integer: 'integer(-5, 5)', @mock\n      image: 'image(\"200x100\")', @mock\n      string: 'string(\"alpha\", 5)', @mock\n      date: 'date', @mock  // iso8601 format // 2021-06-03T07:35:55Z\n      date1: 'date(\"yyyy-mm-dd HH:MM:ss\")' @mock // 2021-06-03 15:35:55\n      date2: 'date(\"unix\")', @mock // unix epoch 1622705755\n      date3: 'date(\"\",\"3 hours 15 minutes\")', @mock \n      date4: 'date(\"\",\"2 weeks ago\")', @mock \n      ipv6: 'ipv6', @mock\n      sentence: 'sentence', @mock\n      cnsentence: 'cnsentence', @mock \n    }\n  }\n}\n```\n\n### @file\n\n**Use file**\n\u003e scope: unit req block\n\n```\n{\n  test1: {\n    req: {\n      headers: {\n        'content-type': 'multipart/form-data',\n      },\n      body: {\n        field: 'my value',\n        file: 'bar.jpg', @file // upload file `bar.jpg`\n      }\n    },\n  }\n}\n```\n### @trans\n\n**Transform data**\n\u003e scope: unit block\n\n```\n{\n  test1: { @client(\"echo\")\n    req: {\n      v1: { @trans(`JSON.stringify($)`)\n        v1: 1,\n        v2: 2,\n      }\n    },\n    res: {\n      v1: `{\"v1\":1,\"v2\":2}`,\n    }\n  },\n  test2: { @client(\"echo\")\n    req: {\n      v1: `{\"v1\":1,\"v2\":2}`,\n    },\n    res: {\n      v2: { @trans(`JSON.parse($)`)\n        v1: 1,\n        v2: 2,\n      }\n    }\n  }\n}\n```\n\n###  @every\n\n**A set of assertions are passed before the test passes**\n\u003e scope: unit res block\n\n```\n{\n  test1: { @client(\"echo\")\n    req: {\n      v1: \"integer(1, 10)\", @mock\n    },\n    res: {\n      v1: [ @every\n        \"$ \u003e -1\", @eval\n        \"$ \u003e 0\", @eval\n      ]\n    }\n  }\n\n}\n```\n\n### @some\n\n**If one of a set of assertions passes, the test passes**\n\u003e scope: unit res block\n\n```\n{\n  test1: { @client(\"echo\")\n    req: {\n      v1: \"integer(1, 10)\", @mock\n    },\n    res: {\n      v1: [ @some\n        \"$ \u003e -1\", @eval\n        \"$ \u003e 10\", @eval\n      ]\n    }\n  }\n}\n```\n\n### @partial\n\n**Mark only partial verification instead of congruent verification**\n\u003e scope: unit res block\n\n```\n{\n  test1: { @client(\"echo\")\n    req: {\n      v1: 2,\n      v2: \"a\",\n    },\n    res: { @partial\n      v1: 2,\n    }\n  },\n  test2: { @client(\"echo\")\n    req: {\n      v1: [\n        1,\n        2\n      ]\n    },\n    res: {\n      v1: [ @partial\n        1\n      ]\n    }\n  }\n}\n```\n\n### @type\n\n**Mark only verifies the type of data**\n\u003e scope: unit res block\n\n```\n{\n  test1: { @client(\"echo\")\n    req: {\n      v1: null,\n      v2: true,\n      v3: \"abc\",\n      v4: 12,\n      v5: 12.3,\n      v6: [1, 2],\n      v7: {a:3,b:4},\n    },\n    res: {\n      v1: null, @type\n      v2: false, @type\n      v3: \"\", @type\n      v4: 0, @type\n      v5: 0.0, @type\n      v6: [], @type\n      v7: {}, @type\n    }\n  },\n}\n```\n\n### @optional\n\n**Marker field is optional**\n\u003e scope: unit res block\n\n```\n{\n  test1: { @client(\"echo\")\n    req: {\n      v1: 3,\n      // v2: 4, optional field\n    },\n    res: {\n      v1: 3,\n      v2: 4, @optional\n    }\n  }\n}\n```\n\n### @nullable\n\n**Marker field can be null**\n\u003e scope: unit res block\n\n```\n{\n  test1: { @client(\"echo\")\n    req: {\n      v1: null,\n      // v1: 3,\n    },\n    res: {\n      v1: 3, @nullable\n    }\n  }\n}\n```\n\n\n## Run\n\nIn some scenarios, use cases may not need to be executed, or they may need to be executed repeatedly. It is necessary to add a `run` option to support this feature.\n\n### Skip\n\n```\n{\n  test1: { @client(\"echo\")\n    req: {\n    },\n    run: {\n      skip: `mod1.test1.res.status === 200`, @eval\n    }\n  }\n}\n```\n\n- `run.skip` skip the test when true\n\n### Delay\n\nRun the test case after waiting for a period of time\n\n```\n{\n  test1: { @client(\"echo\")\n    req: {\n    },\n    run: {\n      delay: 1000,\n    }\n  }\n}\n```\n\n- `run.delay` delay in ms\n\n### Retry\n\n```\n{\n  test1: { @client(\"echo\")\n    req: {\n    },\n    run: {\n      retry: {\n        stop:'$run.count \u003e 2', @eval\n        delay: 1000,\n      }\n    },\n  }\n}\n```\n\nvariables:\n- `$run.count` records the number of retries.\n\noptions:\n- `run.retry.stop`  whether to stop retry\n- `run.retry.delay` interval between each retry (ms)\n\n### Loop\n\n```\n{\n  test1: { @client(\"echo\")\n    req: {\n      v1:'$run.index', @eval\n      v2:'$run.item', @eval\n    },\n    run: {\n      loop: {\n        delay: 1000,\n        items: [\n          'a',\n          'b',\n          'c',\n        ]\n      }\n    },\n  }\n}\n```\n\nvariables:\n- `$run.item` current loop data\n- `$run.index` current loop data index\n\noptions:\n- `run.loop.items` iter pass to `$run.item`\n- `run.loop.delay`  interval between each cycle (ms)\n\n### Dump\n\n```\n{\n  test1: { @client(\"echo\")\n    req: {\n    },\n    run: {\n      dump: true,\n    }\n  }\n}\n```\n\n- `run.dump` force print req/res data when true\n\n\n## Client\n\nThe `req` and `res` data structure of the test case is defined by the client\n\nThe client is responsible for constructing a request according to `req`, sending it to the server, receiving the response from the server, and constructing `res` response data.\n\nIf the test case does not use the `@client` annotation to specify a client, the client is the default.\n\nIf there is no default client defined in the entry file. Apitest will automatically insert `@client({name:\"default\",kind:\"http\"})` with `http` as the default client\n\nApitest provides two kinds of clients.\n\n### Echo\n\nThe `echo` client does not send any request, and directly returns the data in the `req` part as the `res` data.\n\n```\n{\n   test1: {@client(\"echo\")\n     req: {// Just fill in any data\n     },\n     res: {// equal to req\n     }\n   }\n}\n```\n\n### Http\n\n`http` client handles http/https requests/responses.\n\n```\n{\n  test1: { @client({options:{timeout: 10000}}) // Custom client parameters\n    req: {\n      url: \"https://httpbin.org/anything/{id}\", // request url\n      // http methods, `post`, `get`, `delete`, `put`, `patch`\n      method: \"post\",\n      query: { // ?foo=v1\u0026bar=v2\n        foo: \"v1\",\n        bar: \"v2\",\n      },\n\n      // url path params, `/anything/{id}` =\u003e `/anything/33`\n      params: {\n        id: 33, \n      },\n      headers: {\n        'x-key': 'v1'\n      },\n      body: { // request body\n      }\n    },\n    res: {\n      status: 200,\n      headers: {\n        'x-key': 'v1'\n      },\n      body: { // response body\n\n      }\n    }\n  }\n}\n```\n\n#### Options\n\n```js\n{\n  // `baseURL` will be prepended to `url` unless `url` is absolute.\n  baseURL: '',\n  // `timeout` specifies the number of milliseconds before the request times out.\n  // If the request takes longer than `timeout`, the request will be aborted.\n  timeout: 0,\n  // `maxRedirects` defines the maximum number of redirects to follow in node.js. \n  // If set to 0, no redirects will be followed.\n  maxRedirects: 0,\n  // `headers` is default request headers\n  headers: {\n  },\n  // `proxy` configures http(s) proxy, you can also use HTTP_PROXY, HTTPS_PROXY \n  // environment variables\n  proxy: \"http://user:pass@localhost:8080\"\n}\n```\n\n#### Cookies\n\n```js\n{\n  test1: {\n    req: {\n      url: \"https://httpbin.org/cookies/set\",\n      query: {\n        k1: \"v1\",\n        k2: \"v2\",\n      },\n    },\n    res: {\n      status: 302,\n      headers: { @partial\n        'set-cookie': [], @type\n      },\n      body: \"\", @type\n    }\n  },\n  test2: {\n    req: {\n      url: \"https://httpbin.org/cookies\",\n      headers: {\n        Cookie: `test1.res.headers[\"set-cookie\"]`, @eval\n      }\n    },\n    res: {\n      body: { @partial\n        cookies: {\n          k1: \"v1\",\n          k2: \"v2\",\n        }\n      }\n    },\n  },\n}\n```\n\n#### x-www-form-urlencoded \n\nAdd the request header `\"content-type\": \"application/x-www-form-urlencoded\"`\n\n```\n{\n  test2: { @describe('test form')\n    req: {\n      url: \"https://httpbin.org/post\",\n      method: \"post\",\n      headers: {\n        'content-type':\"application/x-www-form-urlencoded\"\n      },\n      body: {\n        v1: \"bar1\",\n        v2: \"Bar2\",\n      }\n    },\n    res: {\n      status: 200,\n      body: { @partial\n        form: {\n          v1: \"bar1\",\n          v2: \"Bar2\",\n        }\n      }\n    }\n  },\n}\n```\n\n#### multipart/form-data\n\n\nAdd the request header `\"content-type\": \"multipart/form-data\"`\nCombined with `@file` annotation to implement file upload\n\n```\n{\n  test3: { @describe('test multi-part')\n    req: {\n      url: \"https://httpbin.org/post\",\n      method: \"post\",\n      headers: {\n        'content-type': \"multipart/form-data\",\n      },\n      body: {\n        v1: \"bar1\",\n        v2: \"httpbin.jsona\", @file\n      }\n    },\n    res: {\n      status: 200,\n      body: { @partial\n        form: {\n          v1: \"bar1\",\n          v2: \"\", @type\n        }\n      }\n    }\n  }\n}\n```\n\n#### graphql\n\n```\n{\n  vars: { @describe(\"share variables\") @client(\"echo\")\n    req: {\n      v1: 10,\n    }\n  },\n  test1: { @describe(\"test graphql\")\n    req: {\n      url: \"https://api.spacex.land/graphql/\",\n      body: {\n        query: `\\`query {\n  launchesPast(limit: ${vars.req.v1}) {\n    mission_name\n    launch_date_local\n    launch_site {\n      site_name_long\n    }\n  }\n}\\`` @eval\n      }\n    },\n    res: {\n      body: {\n        data: {\n          launchesPast: [ @partial\n            {\n              \"mission_name\": \"\", @type\n              \"launch_date_local\": \"\", @type\n              \"launch_site\": {\n                \"site_name_long\": \"\", @type\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}\n```\n\n## Cli\n\n```\nusage: apitest [options] [target]\n\nOptions:\n  -h, --help     Show help                                             [boolean]\n  -V, --version  Show version number                                   [boolean]\n      --ci       Whether to run in ci mode                             [boolean]\n      --reset    Whether to continue with last case                    [boolean]\n      --dry-run  Check syntax then print all cases                     [boolean]\n      --env      Specific test enviroment like prod, dev                [string]\n      --only     Run specific module/case                               [string]\n      --dump     Force print req/res data                              [boolean]\n```\n\n### Multiple Test Environments\n\nApitest supports multiple test environments, which can be specified by the `--env` option.\n\n```\n// Pre-release environment main.jsona\n{\n   @client({\n     options: {\n       url: \"http://pre.example.com/api\"\n     }\n   })\n   @module(\"mod1\")\n}\n```\n\n```\n// Local environment main.local.jsona\n{\n   @client({\n     options: {\n       url: \"http://localhost:3000/api\"\n     }\n   })\n   @module(\"mod1\")\n   @module(\"mod2\") // Only local test module\n}\n```\n\n```sh\n# By default, tests/main.local.jsona is selected\napitest tests\n# Select tests/main.local.jsona\napitest tests --env local\n```\n\nApitest allows to specify main.jsona\n```sh\napitest tests/main.jsona\napitest tests/main.local.jsona\n```\n\nSpecify a specific main.jsona, you can still use the `--env` option\n```sh\n# Select tests/main.local.jsona\napitest tests/main.jsona --env local\n```\n\n### Normal Mode\n\n- Start execution from the last failed test case, print error details and exit when encountering a failed test case\n- If there is option `--reset`, it will start from the beginning instead of where it failed last time\n- If there is the option `--only mod1.test1`, only the selected test case will be executed\n\n### CI Mode\n\n- Ignore the cache and execute the test case from scratch\n- Continue to execute the failed test case\n- After all test cases are executed, errors will be printed uniformly\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsigoden%2Fapitest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsigoden%2Fapitest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsigoden%2Fapitest/lists"}