Giter VIP home page Giter VIP logo

htte's Introduction

HTTE - 文档驱动的接口测试框架

Node Version Build Status dependencies Status Known Vulnerabilities

htte-run-realworld

快速开始

安装 HTTE 命令行

npm i htte-cli -g

编写测试

编写配置 config.yaml

modules:
- echo

编写测试 echo.yaml

- describe: echo get
  req:
    url: https://postman-echo.com/get
    query:
      foo1: bar1
      foo2: bar2
  res:
    body:
      args:
        foo1: bar1
        foo2: bar2
      headers: !@exist object
      url: https://postman-echo.com/get?foo1=bar1&foo2=bar2
- describe: echo post
  req:
    url: https://postman-echo.com/post
    method: post
    body:
      foo1: bar1
      foo2: bar2
  res:
    body: !@object
      json:
        foo1: bar1
        foo2: bar2

运行测试

htte config.yaml

执行结果

✔  echo get (1s)
✔  echo post (1s)

2 passed (2s)

初衷

为什么接口需要测试?

  • 提高服务质量,减少 Bug
  • 更早定位 Bug,节省调试和处理时间
  • 更容易进行代码变更和重构
  • 测试也是文档,有助于熟悉服务功能和逻辑
  • 服务验收标准

有许多项目却没有接口测试,因为测试难,难在:

  • 编写测试让工作量翻倍
  • 编写测试代码需要一定的学习成本
  • 接口间数据耦合使测试不容易编写
  • 构造请求数据和校验响应数据本身就很枯燥繁琐
  • 测试代码也是代码,不花精力优化迭代也会腐化

有没有一条策略,既能让我们享受到测试带来的益处,又能最大程度的降低其成本呢?

研究后得出的答案是文档驱动。

以文档描述测试,用工具执行文档。

这就是 HTTE 诞生的初衷。

文档驱动优点

更容易读

有这样一个接口:它的服务地址是 http://localhost:3000/add,采用 POST 并使用 json 作为数据交换格式,请求数据格式 { a: number, b: number},返回数据格式 {c: number},实现功能是对 ab 求和并返回结果 c

针对这个接口,测试思路: 向这个接口传递数据 {"a":3,"b":4},并期待它返回{"c":7}

这个测试以文档的形式在 HTTE 中是这样写的。

- describe: 两个数相加
  req:
    url: http://localhost:3000/add
    method: post
    headers:
      Content-Type: application/json
    body:
      a: 3
      b: 4
  res:
    body:
      c: 7

直接罗列请求和响应,再加一段描述说明测试是干什么的,一个完整的测试就编写完成了。

更容易读

请看下面两个测试,猜猜目标接口实现了什么功能。

- describe: 登录
  name: fooLogin
  req:
    url: /login
    method: post
    body:
      email: [email protected]
      password: '123456'
  res:
    body:
      token: !@exist string
- describe: 更改昵称
  req:
    url: /user
    method: put
      Authorization: !$conat [Bearer, ' ', !$query fooLogin.res.body.token]
    body:
      nickname: bar
  res:
    body:
      msg: ok

尽管你现在可能还不理解 !@exist, !$concat, !$query,但应该能粗略明白这两个接口的功能、请求响应数据格式。

由于测试逻辑由文档承载,HTTE 轻而易举获得一些其它框架梦寐以求的优点:

编程语言无关性

完全不需要 care 后端是那种语言实现的,再也不担心从一门语言切换到另一门语言,更遑论从一个框架切换到另一个框架了。

技能要求低,上手快

纯文档,不需要懂后端的技术栈,甚至不需要会编程。小白员工,甚至是文职都能快速掌握并编写。

效率高,开发快

容易写,又容易读,技能要求又低,当然写起来快了。终于能自由的享用测试带来的优点,又最大的避免测试带来的麻烦。

天然适合测试驱动开发

文档写起来又快又容易,很方便采用 TDD 开发策略。终于可以无副作用享受 TDD 的优点了。

作为前端接口使用说明

即使有 swagger/blueprint 文档了但还是不会用接口怎么办?把测试文档扔给他,满满的都是例子。

作为后端需求文档/开发指导文件

初入职员工或初级工程师可能没有那么熟悉业务,技能也没有那么熟练,需要较长的学习期或适应期,写出的接口质量可能也不过关。 有这样一份测试文档能大大缩短这段时间,提高接口质量。

HTTE 优点

HTTE 除了拥有文档驱动测试的全部优点外,还拥有以下优点.

使用 YAML 语言

不是引入新的 DSL,而是直接采用 YAML。没有额外的学习成本,也更容易上手,还能能享受现有 YAML 工具和生态。

使用插件灵活生成请求校验响应

先说说为什么文档驱动的测试中需要插件。

某个接口有重名检测,所以测试时我们需要生成一个随机字符串,如何在文档描述随机数呢?某个接口返回一个过期时间,我们需要校验这个时间是再当前时间 24 小时之后,如何再文档中定义这个时间呢?

文档驱动测试这一策略最大的阻碍就是文档无法承载复杂逻辑,缺乏灵活性,它很难描述随机字符串,当前时间这些概率。只有函数才能提供这种灵活性。 插件就是为文档提供函数的,提供这种灵活性的。

插件以 YAML 自定义标签的形式呈现。

有这样一段代码

req:
  body: !$concat [a, b, c]
res:
  body: !@regexp \w{3}

!$concat!@regexp 就是 YAML 标签,它是一种用户自定义的数据类型。再 HTTE 中,其实就是函数。 所以上面的代码再 HTTE 看来是这样子的。

{
  req: {
    body: function(ctx) {
      return (function(literal) {
          return literal.join('');
      })(['a', 'b', 'c'])
    }
  }
  res: {
    body: function(ctx, actual) {
      (function(literal) {
          let re = new Regexp(literal);
          if (!re.test(actual)) {
              ctx.throw('不匹配正则')
          }
      })('\w{3}')
    }
  }
}

总的来说,文档具有易读易写易上手等优点,但无法承载复杂逻辑,不够灵活;而函数/代码能提供这种灵活性,但又带来过多的复杂性。 HTTE 采用 YAML 格式,将函数封装到 YAML 标签中, 协调了这种矛盾,最大程度融合彼此的优点,并几乎规避的彼此的缺点。 这是 HTTE 最大的创新之处了。

接口测试中关于数据主要有两种操作,构造请求和校验响应。所以 HTTE 中存在两种插件。

  • 构造器(resolver),用来构造数据,标签前缀 !$
  • 比对器(differ),用来比对校验数据,标签前缀 !@

插件集:

  • builtin - 包含一些基本常用的插件

组件化,易扩展

HTTE 架构图如下:

archetecture

每个组件都是一个独立的模块,对立完成一项具体的工作。所以能很轻易的替换,也很容易进行扩展。

下面结合一个例子,介绍一个测试单元在 HTTE 中具体执行过程,以便大家熟悉各个组件的功能。

又这样一个测试

- describe:
  req:
    body:
      v: !$randnum [3, 6]
  res:
    body:
      v:  !@compare
        op: gt
        value: 2

在被 Runner 载入后所有 YAML 标签依据插件定义展开成函数,伪代码如下。与此同时 Runner 会发送 runUnit 事件。

{
  req: { // Literal Req
    body: {
      v: function(ctx) {
        return (function(literal) {
          let [min, max] = literal
          return Math.random() * (max - min) + min;
        })([3, 6])
      }
    }
  },
  res: { // Expect Res
    body: {
      v: function(ctx, actual) {
        (function(literal) {
          let { op, value } = literal
          if (op === 'gt') fn = (v1, v2) => v1 > v2;
          if (fn(actual, literal)) return;
          ctx.throw('test fail');
        })({op: 'gt', value: 2})
      }
    }
  }
}

RunnerLiteral Req 传递给 ResolverResolver 的工作就是递归遍历 Req 中的函数并执行,得到一个纯值的数据。并传递给 Client

req: {
  // Resolved Req
  body: {
    v: 5;
  }
}

Client 收到这个数据后,构造请求,并将数据编码为合适的格式(如果是JSONEncoded Req 将变成 {"v":5}),并发送给后端接口服务。 Client 收到后端服务返回的响应后,需要先将数据解码。假设这个接口是一个回显服务,返回的数据是{"v":5}(Raw Res)且为JSONClient 会将数据解码为:

res: {
  // Decoded Res
  body: {
    v: 5;
  }
}

Differ 这时将拿到来自 RunnerExpected Res 和 来自 ClientDecoded Res。它的工作就是将两者进行比对。

Differ 会遍历 Expected Res 中的每一个值,逐一于 Decoded Res 进行比对。有任意一处不相等都会抛出错误,标记测试失败。 如果两者都是值,判断是否它们全等。如果碰到函数,那么会执行比对函数。伪代码如下:

(function(ctx, actual) {
    (function(literal) {
      let { op, value } = literal
      if (op === 'gt') fn = (v1, v2) => v1 > v2;
      if (fn(actual, literal)) return;
      ctx.throw('test fail');
    })({op: 'gt', value: 2})
  }
})(ctx, 5)

如果比对函数没有抛出错误,表示测试通过。Runner 收到测试通过的结果后将发送 doneUnit 事件,并执行队列中的下一条测试。

Reporter 监听 Runner 发送的事件,生成相应的报告,或打印到终端,或生成 HTML 报告文件。

接口协议可扩展,目前已支持 HTTP/GRPC

接口协议由客户端扩展提供。

  • htte - 适用于 HTTP 接口测试
  • grpc - 适用于 GRPC 接口测试

报告生成器可扩展,目前已支持 CLI/HTML

  • cli - 输出到命令行
  • html - 以 HTML 文件的形式输出测试报告

优雅解决的接口数据耦合

接口间数据是存在耦合的。常见例子,先登录拿到 TOKEN 之后才有权限下订单,发朋友圈等。

所以一个接口测试常常需要访问另一个测试的数据。

HTTE 通过会话 + 插件处理这个问题。

还是结合例子来说明。

有一个登录接口,是这样的。

- describe: tom login
  name: tomLogin # <--- 为测试注册一个名字, Why?
  req:
    body:
      email: [email protected]
      password: tom...
  res:
    body:
      token: !@exist string

有一个修改用户名的接口,它是权限接口,必须有个 Authorization 请求头且带上登录返回的 token 才能使用。

- describe: tom update username to tem
  req:
    headers:
      Authorization: !$conat [Bearer, ' ', token?] # <--- 如何当上 TOKEN 呢?
    body:
      username: tem

揭晓答案

      Authorization: !$conat [Bearer, ' ', !$query tomLogin.res.body.token]

还可以通过 tomLogin.req.body.email 获取邮箱值,通过 tomLogin.req.body.password 获取密码。是不是优雅?

这是如何实现的呢?

HTTE 中 Runner 启动后,会初始化会话。每次执行完一条单元测试后,会将执行结果记录在会话中,包括请求数据,响应数据,耗费时间,测试结果等。这些数据是只读的,并作为ctx的暴露给了插件函数,所以插件能访问于它之前执行的测试的数据。

同一个测试中, res 中也是能引用 req 中的数据的。

- describe: res ref data in req
  req:
    body: !$randstr
  res:
    body: !@query req.body

使用宏减少重复书写

围绕一个接口常常会由复数个单元测试,而接口有一些一致的属性,拿 HTTP 接口举例,有 req.url, req.method, req.type,难道每次调用都要重新写一遍吗?

- decribe: add api condition 1
  req:
    url: /add
    method: put
    type: json
    body: v1...
- decribe: add api condition 2
  req:
    url: /add
    method: put
    type: json
    body: v2...

宏就是为了解决这种重复输入问题而引入的。使用也很简单,定义+引用。

在项目配置中定义宏。

defines:
  add: # <-- 定义宏
    req:
      url: /add
      method: put
      type: json

任意地方再用到这个接口,只需要这样了

- describe: add api with macro
  includes: add # <-- 引用宏
  req:
    body: v...

边调试边开发

这个特性是通过组合命令行选项实现。 相关的两个命令行选项是: --bail 遇到任何测试失败的情况停止执行;--continue 从上次中断的地方继续执行测试。

组合这两个选项,可以让我们无数次重置执行该问题接口,直到调试通过。

配置

可选的配置项:

  • session: 指定持久化会话文件存储位置,一般情况下不用填写,HTTE 会在操作系统暂存文件夹下生成一个项目唯一的临时文件存储会话
  • modules: 提供测试模块文件列表。列表顺序对应执行顺序。HTTE 将查找并加载模块文件中的测试用例。
  • clients: 配置客户端扩展
  • plugins: 配置插件
  • reporters: 配置报告生成器扩展
  • defines: 定义宏

最小配置:

modules:
- auth
- user/order

此时clients, plugins, reporter 都将采用默认值。

完全配置:

session: mysession.json
modules:
- auth
- user/order
clients:
- name: http
  pkg: htte-client-http
  options:
    baseUrl: http://example.com/api
    timeout: 1000
reporters:
- name: cli
  pkg: htte-reporter-cli
  options:
    slow: 1000
plugins:
- name: ''
  pkg: htte-plugin-builtin
defines:
  login:
    req:
      method: post
      url: /users/login

配置补丁用来应对环境差异下的配置变更。比如测试环境写,接口地址为 http://localhost:3000/api;正式环境下需要变更为 https://example.com/api。最好后配置补丁实现。

定义补丁文件,补丁文件命名规则 ..。如果项目配置文件名 htte.yaml,补丁名 prod,则补丁文件名为 htte.prod.yaml

- op: replace
  path: /clients/0/options/baseUrl
  value: https://example.com/api

命令行中使用 --patch 选项选定补丁文件。例如我们要引用 htte.prod.yaml,输入 --patch prod

补丁文件规范 jsonpatch.com

测试单元/组

测试单元是 HTTE 的基本单位。

  • describe: 描述测试的目的
  • name: 定义测试名,方便 !$query!@query 引用,可省略
  • client: 定义使用客户端扩展,如果项目只有一个客户端,可省略
  • includes: 引用宏,可以引用多个
  • metadata: 元标签,HTTE 引擎专用数据
    • skip: 是否跳过这条测试
    • debug: 为真表示报告时打印的请求和响应数据详情
    • stop: 为真表示执行该条测试后终止后续操作
  • req: 请求,查阅对应客户端扩展的文档填写
  • res: 响应,查阅对应客户端扩展的文档填写

有时一项功能测试需要多个测试单元配合才能完成。为了表示这种组合/层级关系,HTTE 引入了组的概念。

  • describe: 描该测试的目的
  • defines: 定义组内专用宏,语法与配置中的全局宏一致
  • units: 组中元素
- descirbe: group
  defines:
    token:
      req:
        headers:
          Authorization: !$conat [Bearer, ' ', !$query u1.req.body.token] 
  units:
  - describe: sub group
    units:
      - descirbe: unit
        name: u1
        metadata:
          debug: true
        includes: login
        req:
          body:
            username: foo
            passwold: p123456
        res:
          body:
            token: !$exist
  - descibe: unit
    includes: [updateUser, token]
    req:
      body:
        username: bar

许可证

MIT

htte's People

Contributors

sigoden avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

htte's Issues

引入 Mock

新建一款插件其使用 mockjs 生成假数据

- describe: mock data
  req:
    body:
      email: !$mock '@EMAIL'
      url: !$mock "@URL('http', 'example.com')"
      arr: !$moco
         'list|1-10': 
           -  'id|+1': 1

Improve !@object

use suffix '?' to mark value is optional.

req:
  body:
    k1: !@exist
    k2?: !@exist

k2 with ? means k2 is optional.

Improve `@arraylike`

@arraylike is able to verify the element at a index or the length of array. It can be more powerful. Add '?' to check an element exists, Add '*' to check each element pass the rule.

a: [1, 2, 3]

We check 2 is existed in array a

a: !@arraylike
  ?: 2

We check each element of array a is large than 0

a: !@arraylike
  *: !@compare
    op: gt
    value: 0

进入子层级时重复打印父层级描述

- describe: root
  units:
    - describe: g0
      units:
        - describe: g0-0
    - describe: g1
      units:
        - describe: g1-0

打印描述现在是这样的

root
  g0
    1) g0-0
root
  g1
    2) g1-0

其实应该是这样的

root
  g0
    1) g0-0
  g1
    2) g1-0

测试 g1-0 不应该重复打印 root

提供 XML 编码器

在 Restful API 数据交换格式中 XML 也很常见,但 Htte 目前只支持 JSON。可以支持 XML 编码吗?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.