{"id":15013998,"url":"https://github.com/sigoden/htte","last_synced_at":"2025-04-09T19:23:06.359Z","repository":{"id":57268046,"uuid":"130996142","full_name":"sigoden/htte","owner":"sigoden","description":"Document Driven API Test Framework","archived":false,"fork":false,"pushed_at":"2020-12-07T10:13:40.000Z","size":1236,"stargazers_count":73,"open_issues_count":2,"forks_count":6,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-06T21:08:54.039Z","etag":null,"topics":["api-test","api-testing","api-testing-framework","grpc-testing","htte","http-test","postman"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","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":"CHANGELOG.md","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":"2018-04-25T11:27:23.000Z","updated_at":"2025-02-21T15:50:20.000Z","dependencies_parsed_at":"2022-09-02T02:50:17.220Z","dependency_job_id":null,"html_url":"https://github.com/sigoden/htte","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sigoden%2Fhtte","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sigoden%2Fhtte/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sigoden%2Fhtte/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sigoden%2Fhtte/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sigoden","download_url":"https://codeload.github.com/sigoden/htte/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248095803,"owners_count":21046912,"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-test","api-testing","api-testing-framework","grpc-testing","htte","http-test","postman"],"created_at":"2024-09-24T19:45:02.914Z","updated_at":"2025-04-09T19:23:06.299Z","avatar_url":"https://github.com/sigoden.png","language":"JavaScript","readme":"# HTTE - 文档驱动的接口测试框架\n\n[![Node Version](https://img.shields.io/badge/node-%3E=6-brightgreen.svg)](https://www.npmjs.com/package/htte)\n[![Build Status](https://travis-ci.org/sigoden/htte.svg?branch=master)](https://travis-ci.org/sigoden/htte)\n[![dependencies Status](https://david-dm.org/sigoden/htte/status.svg)](https://david-dm.org/sigoden/htte)\n[![Known Vulnerabilities](https://snyk.io/test/github/sigoden/htte/badge.svg?targetFile=package.json)](https://snyk.io/test/github/sigoden/htte?targetFile=package.json)\n\n\n![htte-run-realworld](site/images/realworld.gif)\n\n## 快速开始\n\n### 安装 HTTE 命令行\n\n```\nnpm i htte-cli -g\n```\n\n### 编写测试\n\n编写配置 `config.yaml`\n```yaml\nmodules:\n- echo\n```\n\n编写测试 `echo.yaml`\n```yaml\n- describe: echo get\n  req:\n    url: https://postman-echo.com/get\n    query:\n      foo1: bar1\n      foo2: bar2\n  res:\n    body:\n      args:\n        foo1: bar1\n        foo2: bar2\n      headers: !@exist object\n      url: https://postman-echo.com/get?foo1=bar1\u0026foo2=bar2\n- describe: echo post\n  req:\n    url: https://postman-echo.com/post\n    method: post\n    body:\n      foo1: bar1\n      foo2: bar2\n  res:\n    body: !@object\n      json:\n        foo1: bar1\n        foo2: bar2\n```\n\n### 运行测试\n\n```\nhtte config.yaml\n```\n执行结果\n```\n✔  echo get (1s)\n✔  echo post (1s)\n\n2 passed (2s)\n```\n\n## 初衷\n\n为什么接口需要测试？\n\n- 提高服务质量，减少 Bug\n- 更早定位 Bug，节省调试和处理时间\n- 更容易进行代码变更和重构\n- 测试也是文档，有助于熟悉服务功能和逻辑\n- 服务验收标准\n\n有许多项目却没有接口测试，因为测试难，难在：\n\n- 编写测试让工作量翻倍\n- 编写测试代码需要一定的学习成本\n- 接口间数据耦合使测试不容易编写\n- 构造请求数据和校验响应数据本身就很枯燥繁琐\n- 测试代码也是代码，不花精力优化迭代也会腐化\n\n有没有一条策略，既能让我们享受到测试带来的益处，又能最大程度的降低其成本呢？\n\n研究后得出的答案是文档驱动。\n\n以文档描述测试，用工具执行文档。\n\n这就是 HTTE 诞生的初衷。\n\n## 文档驱动优点\n\n### 更容易读\n\n有这样一个接口：它的服务地址是 `http://localhost:3000/add`，采用 `POST` 并使用 `json` 作为数据交换格式，请求数据格式 `{ a: number, b: number}`，返回数据格式 `{c: number}`，实现功能是对 `a`，`b` 求和并返回结果 `c`。\n\n针对这个接口，测试思路： 向这个接口传递数据 `{\"a\":3,\"b\":4}`，并期待它返回`{\"c\":7}`\n\n这个测试以文档的形式在 HTTE 中是这样写的。\n\n```yaml\n- describe: 两个数相加\n  req:\n    url: http://localhost:3000/add\n    method: post\n    headers:\n      Content-Type: application/json\n    body:\n      a: 3\n      b: 4\n  res:\n    body:\n      c: 7\n```\n\n直接罗列请求和响应，再加一段描述说明测试是干什么的，一个完整的测试就编写完成了。\n\n### 更容易读\n\n请看下面两个测试，猜猜目标接口实现了什么功能。\n\n```yaml\n- describe: 登录\n  name: fooLogin\n  req:\n    url: /login\n    method: post\n    body:\n      email: foo@example.com\n      password: '123456'\n  res:\n    body:\n      token: !@exist string\n- describe: 更改昵称\n  req:\n    url: /user\n    method: put\n      Authorization: !$conat [Bearer, ' ', !$query fooLogin.res.body.token]\n    body:\n      nickname: bar\n  res:\n    body:\n      msg: ok\n```\n\n尽管你现在可能还不理解 `!@exist`, `!$concat`, `!$query`，但应该能粗略明白这两个接口的功能、请求响应数据格式。\n\n由于测试逻辑由文档承载，HTTE 轻而易举获得一些其它框架梦寐以求的优点：\n\n### 编程语言无关性\n\n完全不需要 care 后端是那种语言实现的，再也不担心从一门语言切换到另一门语言，更遑论从一个框架切换到另一个框架了。\n\n### 技能要求低，上手快\n\n纯文档，不需要懂后端的技术栈，甚至不需要会编程。小白员工，甚至是文职都能快速掌握并编写。\n\n### 效率高，开发快\n\n容易写，又容易读，技能要求又低，当然写起来快了。终于能自由的享用测试带来的优点，又最大的避免测试带来的麻烦。\n\n### 天然适合测试驱动开发\n\n文档写起来又快又容易，很方便采用 TDD 开发策略。终于可以无副作用享受 TDD 的优点了。\n\n### 作为前端接口使用说明\n\n即使有 swagger/blueprint 文档了但还是不会用接口怎么办？把测试文档扔给他，满满的都是例子。\n\n### 作为后端需求文档/开发指导文件\n\n初入职员工或初级工程师可能没有那么熟悉业务，技能也没有那么熟练，需要较长的学习期或适应期，写出的接口质量可能也不过关。\n有这样一份测试文档能大大缩短这段时间，提高接口质量。\n\n## HTTE 优点\n\nHTTE 除了拥有文档驱动测试的全部优点外，还拥有以下优点.\n\n### 使用 YAML 语言\n\n不是引入新的 DSL，而是直接采用 YAML。没有额外的学习成本，也更容易上手，还能能享受现有 YAML 工具和生态。\n\n### 使用插件灵活生成请求校验响应\n\n先说说为什么文档驱动的测试中需要插件。\n\n某个接口有重名检测，所以测试时我们需要生成一个随机字符串，如何在文档描述随机数呢？某个接口返回一个过期时间，我们需要校验这个时间是再当前时间 24 小时之后，如何再文档中定义这个时间呢？\n\n文档驱动测试这一策略最大的阻碍就是文档无法承载复杂逻辑，缺乏灵活性，它很难描述随机字符串，当前时间这些概率。只有函数才能提供这种灵活性。\n插件就是为文档提供函数的，提供这种灵活性的。\n\n插件以 YAML 自定义标签的形式呈现。\n\n有这样一段代码\n\n```yaml\nreq:\n  body: !$concat [a, b, c]\nres:\n  body: !@regexp \\w{3}\n```\n\n`!$concat` 和 `!@regexp` 就是 YAML 标签，它是一种用户自定义的数据类型。再 HTTE 中，其实就是函数。\n所以上面的代码再 HTTE 看来是这样子的。\n\n```js\n{\n  req: {\n    body: function(ctx) {\n      return (function(literal) {\n          return literal.join('');\n      })(['a', 'b', 'c'])\n    }\n  }\n  res: {\n    body: function(ctx, actual) {\n      (function(literal) {\n          let re = new Regexp(literal);\n          if (!re.test(actual)) {\n              ctx.throw('不匹配正则')\n          }\n      })('\\w{3}')\n    }\n  }\n}\n```\n\n总的来说，文档具有易读易写易上手等优点，但无法承载复杂逻辑，不够灵活；而函数/代码能提供这种灵活性，但又带来过多的复杂性。\nHTTE 采用 YAML 格式，将函数封装到 YAML 标签中， 协调了这种矛盾，最大程度融合彼此的优点，并几乎规避的彼此的缺点。\n这是 HTTE 最大的创新之处了。\n\n接口测试中关于数据主要有两种操作，构造请求和校验响应。所以 HTTE 中存在两种插件。\n\n- 构造器(resolver)，用来构造数据，标签前缀 `!$`\n- 比对器(differ)，用来比对校验数据，标签前缀 `!@`\n\n插件集:\n\n- [builtin](packages/htte-plugin-builtin) - 包含一些基本常用的插件\n\n### 组件化，易扩展\n\nHTTE 架构图如下:\n\n![archetecture](site/images/architecture.jpg)\n\n每个组件都是一个独立的模块，对立完成一项具体的工作。所以能很轻易的替换，也很容易进行扩展。\n\n下面结合一个例子，介绍一个测试单元在 HTTE 中具体执行过程，以便大家熟悉各个组件的功能。\n\n又这样一个测试\n\n```yaml\n- describe:\n  req:\n    body:\n      v: !$randnum [3, 6]\n  res:\n    body:\n      v:  !@compare\n        op: gt\n        value: 2\n```\n\n在被 `Runner` 载入后所有 YAML 标签依据插件定义展开成函数，伪代码如下。与此同时 `Runner` 会发送 `runUnit` 事件。\n\n```js\n{\n  req: { // Literal Req\n    body: {\n      v: function(ctx) {\n        return (function(literal) {\n          let [min, max] = literal\n          return Math.random() * (max - min) + min;\n        })([3, 6])\n      }\n    }\n  },\n  res: { // Expect Res\n    body: {\n      v: function(ctx, actual) {\n        (function(literal) {\n          let { op, value } = literal\n          if (op === 'gt') fn = (v1, v2) =\u003e v1 \u003e v2;\n          if (fn(actual, literal)) return;\n          ctx.throw('test fail');\n        })({op: 'gt', value: 2})\n      }\n    }\n  }\n}\n```\n\n`Runner` 将 `Literal Req` 传递给 `Resolver`，`Resolver` 的工作就是递归遍历 `Req` 中的函数并执行，得到一个纯值的数据。并传递给 `Client`。\n\n```js\nreq: {\n  // Resolved Req\n  body: {\n    v: 5;\n  }\n}\n```\n\n`Client` 收到这个数据后，构造请求，并将数据编码为合适的格式(如果是`JSON`，`Encoded Req` 将变成 `{\"v\":5}`)，并发送给后端接口服务。\n`Client` 收到后端服务返回的响应后，需要先将数据解码。假设这个接口是一个回显服务，返回的数据是`{\"v\":5}`(`Raw Res`)且为`JSON`，`Client` 会将数据解码为：\n\n```js\nres: {\n  // Decoded Res\n  body: {\n    v: 5;\n  }\n}\n```\n\n`Differ` 这时将拿到来自 `Runner` 的 `Expected Res` 和 来自 `Client` 的 `Decoded Res`。它的工作就是将两者进行比对。\n\n`Differ` 会遍历 `Expected Res` 中的每一个值，逐一于 `Decoded Res` 进行比对。有任意一处不相等都会抛出错误，标记测试失败。\n如果两者都是值，判断是否它们全等。如果碰到函数，那么会执行比对函数。伪代码如下：\n\n```js\n(function(ctx, actual) {\n    (function(literal) {\n      let { op, value } = literal\n      if (op === 'gt') fn = (v1, v2) =\u003e v1 \u003e v2;\n      if (fn(actual, literal)) return;\n      ctx.throw('test fail');\n    })({op: 'gt', value: 2})\n  }\n})(ctx, 5)\n```\n\n如果比对函数没有抛出错误，表示测试通过。`Runner` 收到测试通过的结果后将发送 `doneUnit` 事件，并执行队列中的下一条测试。\n\n`Reporter` 监听 `Runner` 发送的事件，生成相应的报告，或打印到终端，或生成 HTML 报告文件。\n\n### 接口协议可扩展，目前已支持 HTTP/GRPC\n\n接口协议由客户端扩展提供。\n\n- [htte](packages/htte-client-http) - 适用于 HTTP 接口测试\n- [grpc](packages/htte-client-grpc) - 适用于 GRPC 接口测试\n\n### 报告生成器可扩展，目前已支持 CLI/HTML\n\n- [cli](packages/htte-reporter-cli) - 输出到命令行\n- [html](packages/htte-reporter-html) - 以 HTML 文件的形式输出测试报告\n\n### 优雅解决的接口数据耦合\n\n接口间数据是存在耦合的。常见例子，先登录拿到 TOKEN 之后才有权限下订单，发朋友圈等。\n\n所以一个接口测试常常需要访问另一个测试的数据。\n\nHTTE 通过会话 + 插件处理这个问题。\n\n还是结合例子来说明。\n\n有一个登录接口，是这样的。\n\n```yaml\n- describe: tom login\n  name: tomLogin # \u003c--- 为测试注册一个名字， Why?\n  req:\n    body:\n      email: tom@gmail.com\n      password: tom...\n  res:\n    body:\n      token: !@exist string\n```\n\n有一个修改用户名的接口，它是权限接口，必须有个 `Authorization` 请求头且带上登录返回的 `token` 才能使用。\n\n```yaml\n- describe: tom update username to tem\n  req:\n    headers:\n      Authorization: !$conat [Bearer, ' ', token?] # \u003c--- 如何当上 TOKEN 呢？\n    body:\n      username: tem\n```\n\n揭晓答案\n\n```yaml\n      Authorization: !$conat [Bearer, ' ', !$query tomLogin.res.body.token]\n```\n\n还可以通过 `tomLogin.req.body.email` 获取邮箱值，通过 `tomLogin.req.body.password` 获取密码。是不是优雅？\n\n这是如何实现的呢？\n\nHTTE 中 `Runner` 启动后，会初始化会话。每次执行完一条单元测试后，会将执行结果记录在会话中，包括请求数据，响应数据，耗费时间，测试结果等。这些数据是只读的，并作为`ctx`的暴露给了插件函数，所以插件能访问于它之前执行的测试的数据。\n\n同一个测试中, `res` 中也是能引用 `req` 中的数据的。\n\n```yaml\n- describe: res ref data in req\n  req:\n    body: !$randstr\n  res:\n    body: !@query req.body\n```\n\n### 使用宏减少重复书写\n\n围绕一个接口常常会由复数个单元测试，而接口有一些一致的属性，拿 HTTP 接口举例，有 `req.url`, `req.method`, `req.type`，难道每次调用都要重新写一遍吗？\n\n```yaml\n- decribe: add api condition 1\n  req:\n    url: /add\n    method: put\n    type: json\n    body: v1...\n- decribe: add api condition 2\n  req:\n    url: /add\n    method: put\n    type: json\n    body: v2...\n```\n\n宏就是为了解决这种重复输入问题而引入的。使用也很简单，定义+引用。\n\n在项目配置中定义宏。\n\n```yaml\ndefines:\n  add: # \u003c-- 定义宏\n    req:\n      url: /add\n      method: put\n      type: json\n```\n\n任意地方再用到这个接口，只需要这样了\n\n```yaml\n- describe: add api with macro\n  includes: add # \u003c-- 引用宏\n  req:\n    body: v...\n```\n\n### 边调试边开发\n\n这个特性是通过组合命令行选项实现。\n相关的两个命令行选项是: `--bail` 遇到任何测试失败的情况停止执行；`--continue` 从上次中断的地方继续执行测试。\n\n组合这两个选项，可以让我们无数次重置执行该问题接口，直到调试通过。\n\n## 配置\n\n可选的配置项：\n\n- `session`: 指定持久化会话文件存储位置，一般情况下不用填写，HTTE 会在操作系统暂存文件夹下生成一个项目唯一的临时文件存储会话\n- `modules`: 提供测试模块文件列表。列表顺序对应执行顺序。HTTE 将查找并加载模块文件中的测试用例。\n- `clients`: 配置客户端扩展\n- `plugins`: 配置插件\n- `reporters`: 配置报告生成器扩展\n- `defines`: 定义宏\n\n最小配置:\n\n```yaml\nmodules:\n- auth\n- user/order\n```\n\n此时`clients`, `plugins`, `reporter` 都将采用默认值。\n\n完全配置:\n\n```yaml\nsession: mysession.json\nmodules:\n- auth\n- user/order\nclients:\n- name: http\n  pkg: htte-client-http\n  options:\n    baseUrl: http://example.com/api\n    timeout: 1000\nreporters:\n- name: cli\n  pkg: htte-reporter-cli\n  options:\n    slow: 1000\nplugins:\n- name: ''\n  pkg: htte-plugin-builtin\ndefines:\n  login:\n    req:\n      method: post\n      url: /users/login\n```\n\n配置补丁用来应对环境差异下的配置变更。比如测试环境写，接口地址为 `http://localhost:3000/api`；正式环境下需要变更为 `https://example.com/api`。最好后配置补丁实现。\n\n定义补丁文件,补丁文件命名规则 \u003cbase\u003e.\u003cpatch\u003e.\u003cext\u003e。如果项目配置文件名 `htte.yaml`，补丁名 `prod`，则补丁文件名为 `htte.prod.yaml`。\n\n```yaml\n- op: replace\n  path: /clients/0/options/baseUrl\n  value: https://example.com/api\n```\n\n命令行中使用 `--patch` 选项选定补丁文件。例如我们要引用 `htte.prod.yaml`，输入 `--patch prod`\n\n补丁文件规范 [jsonpatch.com](http://jsonpatch.com/)。\n\n## 测试单元/组\n\n测试单元是 HTTE 的基本单位。\n\n- `describe`: 描述测试的目的\n- `name`: 定义测试名，方便 `!$query` 和 `!@query` 引用，可省略\n- `client`: 定义使用客户端扩展，如果项目只有一个客户端，可省略\n- `includes`: 引用宏，可以引用多个\n- `metadata`: 元标签，HTTE 引擎专用数据\n  - `skip`:  是否跳过这条测试\n  - `debug`: 为真表示报告时打印的请求和响应数据详情\n  - `stop`: 为真表示执行该条测试后终止后续操作\n- `req`: 请求，查阅对应客户端扩展的文档填写\n- `res`: 响应，查阅对应客户端扩展的文档填写\n\n\n有时一项功能测试需要多个测试单元配合才能完成。为了表示这种组合/层级关系，HTTE 引入了组的概念。\n\n- `describe`: 描该测试的目的\n- `defines`: 定义组内专用宏，语法与配置中的全局宏一致\n- `units`: 组中元素\n\n```yaml\n- descirbe: group\n  defines:\n    token:\n      req:\n        headers:\n          Authorization: !$conat [Bearer, ' ', !$query u1.req.body.token] \n  units:\n  - describe: sub group\n    units:\n      - descirbe: unit\n        name: u1\n        metadata:\n          debug: true\n        includes: login\n        req:\n          body:\n            username: foo\n            passwold: p123456\n        res:\n          body:\n            token: !$exist\n  - descibe: unit\n    includes: [updateUser, token]\n    req:\n      body:\n        username: bar\n```\n\n## 许可证\n\n[MIT](LICENSE)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsigoden%2Fhtte","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsigoden%2Fhtte","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsigoden%2Fhtte/lists"}