任务
- 1: 快速入门
 - 2: 关于安全
 - 3: 用例模板
 - 4: 任务
 - 5: 测试用例验证
 - 6: End-to-End
 - 7: Mock 服务
 - 8: 代码生成
 - 9: gRPC测试用例编写指南
 - 10: gRPC TLS verification
 - 11: 插件
 
1 - 快速入门
本指南将帮助您通过几个简单的步骤开始使用 API Testing。
执行部分测试用例
下面的命令会执行名称中包含 sbom 的所有测试用例:
atest run -p test-suite.yaml --case-filter sbom
2 - 关于安全
通常在不使用 TLS 证书认证时,gRPC 客户端与服务端之间进行的是明文通信,信息易被第三方监听或篡改。所以大多数情况下均推荐使用 SSL/TLS 保护 gRPC 服务。目前atest已实现服务端 TLS,双向 TLS(mTLS) 需等待后续实现。
默认情况下atest不使用任何安全策略,等价于spec.secure.insecure = true。启用 TLS 仅需 yaml 中添加以下内容:
spec:
  secure:
    cert: server.pem
    serverName: atest
字段说明
secure共有以下五个字段:
| 字段名 | 类型 | 是否可选 | 
|---|---|---|
| cert | string | x | 
| ca | string | √ | 
| key | string | √ | 
| serverName | string | x | 
| insecure | bool | √ | 
cert为客户端需要配置的证书的文件路径,格式为PEM。
serverName为 TLS 所需的服务名,通常为签发证书时使用的 x509 SAN。
ca为 CA 证书的路径,key为与cert对应的私钥,这两项填写后代表启用 mTLS。(mTLS 尚未实现)
当insecure为false时,cert和serverName为必填项。
3 - 用例模板
atest 采用 sprig 作为测试用例的模板引擎。通过模板函数可以生成很多随机数据:
手机号
下面的代码可以生成 182 开头的手机号:
182{{shuffle "09876543"}}
带权重的随机枚举
下面的代码以 80% 的概率返回 open,以 20% 的概率返回 closed:
{{randWeightEnum (weightObject 4 "open") (weightObject 1 "closed")}}
时间
下面的代码可以生成当前时间,并制定时间格式:
{{ now.Format "2006-01-02T15:04:05Z07:00" }}
如果想要获取其他时间,则可以参考如下写法:
{{(now | date_modify "2h") | date "2006-01-02T15:04:05"}}
环境变量
下面的代码可以获取环境变量 SHELL 的值,在需要使用一个全局变量的时候,可以使用这个模板函数:
{{ env "SHELL" }}
4 - 任务
在特定情况下,执行接口测试用例前需要执行相应的任务,例如:数据初始化、等待服务就绪等等。 atest 的任务功能,就是为了满足这类场景而设计的。
任务的执行引擎为 expr,如果当前页面给出的示例无法满足你的需求,可以查阅相关的官方文档。
等待接口响应码为 200
以下的例子会每隔一秒请求一次指定的接口,并检查响应码(Status Code)是否为 200,如果不是的话,则最多会重试 3 次:
name: demo
api: http://localhost
items:
  - name: login
    before:
      items:
        - httpReady("http://localhost/health", 3)
    request:
      api: /demo
如果希望检查响应体的话,则可以用下面的表达式:
httpReady("http://localhost:17001/actuator/health", 3, 'components.discoveryComposite.components.eureka.details.applications["AUTH"] == 1')
5 - 测试用例验证
atest 采用 https://expr.medv.io 对 HTTP 请求响应的验证,比如:返回的数据列表长度验证、具体值的验证等等。下面给出一些例子:
需要注意的是,
data指的是 HTTP Response Body(响应体)的 JSON 对象。
数组长度判断
  - name: projectKinds
    request:
      api: /api/resources/projectKinds
    expect:
      verify:
        - len(data.data)  == 6
数组值检查
检查数组中是否有元素的字段包含特定值
示例数据:
{
  "data": [{
    "key": "Content-Type"
  }]
}
校验配置:
- name: popularHeaders
  request:
    api: /popularHeaders
  expect:
    verify:
      - any(data.data, {.key == "Content-Type"})
检查数组中是否有元素的字段只包含特定值
校验配置:
- name: popularHeaders
  request:
    api: /popularHeaders
  expect:
    verify:
      - all(data.data, {.key == "Content-Type" or .key == "Target"})
更多用法.
字符串判断
- name: metrics
  request:
    api: |
      {{.param.server}}/metrics
  expect:
    verify:
      - indexOf(data, "atest_execution_count") != -1
更多用法.
6 - End-to-End
atest 非常适合针对(HTTP)接口做 E2E(端到端)测试,E2E 测试可以确保后端接口在完整的环境中持续地保持正确运行。下面采用 Docker compose 给出一个使用事例:
version: '3.1'
services:
  testing:
    image: ghcr.io/linuxsuren/api-testing:latest
    environment:
      SERVER: http://server:8080
    volumes:
      - ./testsuite.yaml:/work/testsuite.yaml
    command: atest run -p /work/testsuite.yaml
    depends_on:
      server:
        condition: service_healthy
    links:
      - server
  server:
    image: ghcr.io/devops-ws/learn-springboot:master
    healthcheck:
      test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/8080"]
      interval: 3s
      timeout: 60s
      retries: 10
      start_period: 3s
从 Docker compose v2.36.0 开始,可以采用如下简化的写法:
services:
  testing:
    scale: 0
    provider:
      type: atest
      options:
        pattern: testsuite.yaml
    environment:
      SERVER: http://server:8080
    depends_on:
      server:
        condition: service_healthy
    links:
      - server
  server:
    image: ghcr.io/devops-ws/learn-springboot:master
    healthcheck:
      test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/8080"]
      interval: 3s
      timeout: 60s
      retries: 10
      start_period: 3s
7 - Mock 服务
Mock 服务在前后端并行开发、系统对接、设备对接场景下能起到非常好的作用,可以极大地降低团队之间、系统之间的耦合度。
用户可以通过命令行终端(CLI)、Web UI 的方式来使用 Mock 服务。
命令行
atest mock --prefix / --port 9090 mock.yaml
Web
在 UI 上可以实现和命令行相同的功能,并可以通过页面编辑的方式修改、加载 Mock 服务配置。
Mock Docker Registry
您可以通过执行下面的命令 mock 一个容器仓库服务container registry:
atest mock --prefix / mock/image-registry.yaml
之后,您可以通过使用如下的命令使用 mock 功能。
docker pull localhost:6060/repo/name:tag
语法
从整体上来看,我们的写法和 HTTP 的名称基本保持一致,用户无需再了解额外的名词。此外,提供两种描述 Mock 服务的方式:
- 面向对象的 CRUD
 - 自定义 HTTP 服务
 
面向对象
#!api-testing-mock
# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-mock-schema.json
objects:
  - name: projects
    initCount: 3
    sample: |
      {
        "name": "atest",
        "color": "{{ randEnum "blue" "read" "pink" }}"
      }
上面 projects 的配置,会自动提供该对象的 CRUD(创建、查找、更新、删除)的 API,你可以通过 atest 或类似工具发出 HTTP 请求。例如:
curl http://localhost:6060/mock/projects
curl http://localhost:6060/mock/projects/atest
curl http://localhost:6060/mock/projects -X POST -d '{"name": "new"}'
curl http://localhost:6060/mock/projects -X PUT -d '{"name": "new", "remark": "this is a project"}'
curl http://localhost:6060/mock/projects/atest -X DELETE
initCount是指按照sample给定的数据初始化多少个对象;如果没有指定的话,则默认值为 1.
自定义
#!api-testing-mock
# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-mock-schema.json
items:
  - name: prList
    request:
      path: /api/v1/repos/{repo}/prs
    response:
      header:
        server: mock
        Content-Type: application/json
      body: |
        {
          "count": 1,
          "repo": "{{.Param.repo}}",
          "items": [{
            "title": "fix: there is a bug on page {{ randEnum "one", "two" }}",
            "number": 123,
            "message": "{{.Response.Header.server}}",
            "author": "someone",
            "status": "success"
          }]
        }
启动 Mock 服务后,我们就可以发起如下的请求:
curl http://localhost:6060/mock/api/v1/repos/atest/prs -v
编码器
另外,为了满足复杂的场景,还可以对 Response Body 做特定的解码,目前支持:base64、url、raw:
encoder 为
raw时,表示不进行处理
#!api-testing-mock
# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-mock-schema.json
items:
  - name: base64
    request:
      path: /api/v1/base64
    response:
      body: aGVsbG8=
      encoder: base64
上面 Body 的内容是经过 base64 编码的,这可以用于不希望直接明文显示,或者是图片的场景:
curl http://localhost:6060/mock/api/v1/base64
如果你的 Body 内容可以通过另外一个 HTTP 请求(GET)获得,那么你可以这么写:
#!api-testing-mock
# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-mock-schema.json
items:
  - name: baidu
    request:
      path: /api/v1/baidu
    response:
      body: https://baidu.com
      encoder: url
如果你的响应内容比较大,或者保存在一个本地文件中,那么你可以这么写:
#!api-testing-mock
# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-mock-schema.json
items:
  - name: baidu
    request:
      path: /api/v1/baidu
    response:
      bodyFromFile: /tmp/baidu.html
通过下面的方式也可以生成图片:
items:
- name: image
    request:
      path: /v1/image
    response:
      header:
        Content-Type: image/png
      body: |
        {{ randImage 300 300 }}
条件判断
对于查询类的 API,通常会接收参数,并根据参数的不同,返回相应的数据。这时候,可以用到条件判断的表达式:
items:
  - name: cats
    request:
      path: /api/v1/cats/{size}
    response:
      header:
        Content-Type: application/json
      body: |
        {{if eq .Param.size "big"}}
          {
            "name": "big cat"
          }
        {{else if eq .Param.size "middle"}}
          {
            "name": "middle cat"
          }
        {{else if eq .Param.size "small"}}
          {
            "name": "small cat"
          }
        {{end}}
下面是一个根据请求 Payload 给出不同响应的例子:
items:
  - name: createBook
    request:
      path: /v1/books
      method: POST
      body: |
        {
          "size": "big"
        }
    response:
      body: |
        {{if eq (fromJson .Param._payload).size "big"}}
        {
          "name": "big book"
        }
        {{else if eq (fromJson .Param._payload).size "small"}}
        {
          "name": "small book"
        }
        {{else}}
        {
          "name": "unknown book"
        }
        {{end}}
代理
在实际情况中,往往是向已有系统或平台添加新的 API,此时要 Mock 所有已经存在的 API 就既没必要也需要很多工作量。因此,我们提供了一种简单的方式,即可以增加代理的方式把已有的 API 请求转发到实际的地址,只对新增的 API 进行 Mock 处理。如下所示:
#!api-testing-mock
# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-mock-schema.json
proxies:
  - path: /api/v1/{part}
    target: http://atest.localhost:8080
    echo: true
当 echo 的值为 true 时,会把收到的请求以及响应打印出来,方便观察数据。
当我们发起如下的请求时,实际请求的地址为 http://atest.localhost:8080/api/v1/projects
curl http://localhost:6060/mock/api/v1/projects
如何希望把所有的请求都转发到某个地址,则可以使用通配符的方式:
proxies:
  - path: /{path:.*}
    target: http://192.168.123.58:9200
TCP 协议代理
proxies:
  - protocol: tcp
    port: 3306
    path: /
    target: 192.168.123.58:33060
代理多个服务
atest mock-compose bin/compose.yaml
执行上面的命令,会启动多个 Mock 代理服务,分别以不同的端口代理了 Elasticsearch 和 Eureka 服务:
proxies:
  - prefix: /
    port: 9200
    path: /{path:.*}
    target: http://192.168.123.58:9200
  - prefix: /
    port: 17001
    path: /{path:.*}
    target: http://192.168.123.58:17001
  - protocol: tcp
    port: 33060
    path: /
    target: 192.168.123.58:33060
当前代理支持 HTTP 和 TCP 协议,上面的例子中代理了 MySQL 的 33060 端口。
Webhook
有些场景下,需要定时向服务器发送请求,这时可以使用 Webhook。当前支持的协议包括:
- HTTP
 - Syslog
 
webhooks:
  - timer: 3s
    name: shakeHands
    request:
      method: POST
      path: http://192.168.1.123:8080/api/v1
      header:
        Content-Type: application/json
      bodyFromFile: demo.json
更多 URL 中通配符的用法,请参考 https://github.com/gorilla/mux
9 - gRPC测试用例编写指南
本文档将介绍如何编写api-testing的 gRPC API 的测试用例。
阅读本文档之前,您需要先安装并配置好api-testing,具体操作可以参考安装章节。如果您已经完成了这些步骤,可以继续阅读本文档的后续部分。
创建测试项目
创建一个基于服务反射的 gRPC 测试用例仅需在 yaml 文件的spec路径下加入以下内容:
spec:
  rpc:
    serverReflection: true
rpc字段一共有五个子字段
| 字段名 | 类型 | 是否可选 | 
|---|---|---|
| import | []string | √ | 
| protofile | string | √ | 
| protoset | string | √ | 
| serverReflection | bool | √ | 
字段import和protofile
protofile是一个文件路径,指向api-testing查找描述符的.proto文件的位置。
import字段与protc编译器中的--import_path参数类似,用于确定查找proto文件的位置与解析依赖的目录。与protoc一样,您不需要在此处指定某些proto文件的位置(以google.protobuf开头的Protocol Buffers Well-Known Types),它们已经内置在了api-testing中。
字段protoset
protoset字段既可以是一个文件路径,也可以是http(s)://开头的网络地址。
当您的proto数量繁多或引用关系复杂,可以尝试使用protoc --descriptor_set_out=set.pb生成proto描述符集合。本质上它是一个使用了wire编码的二进制文件,其中包括了所有需要的描述符。
字段serverReflection
若目标服务器支持服务反射,将此项设为true则不再需要提供上述三个字段。
注:api-testing对三种描述符来源的优先级顺序为
serverReflection > protoset > protofile
编写gRPC API测试
与编写HTTP测试用例类型,您需要在根节点的api字段定义服务器的地址。
api: 127.0.0.1:7070
默认情况下api-testing使用不安全的方式连接到目标服务器。若您想配置TLS证书,请参考文档关于安全
编写gRPC API测试的方式与编写HTTP API测试的方式基本相同。
- name: FunctionsQuery
  request:
    api: /server.Runner/FunctionsQuery
    body: |
      {
        "name": "hello"
      }
  expect:
    body: |
      {
        "data": [
          {
            "key": "hello",
            "value": "func() string"
          }
        ]
      }
api字段的格式为/package.service/method,支持 gRPC 一元调用、客户端流、服务端流和双向流调用。
与api字段同级的body字段是以JSON格式表示的Protocol Buffers消息,代表将要调用的api的入参。特别的,当您需要调用客户端流或双向流 API 时,请使用JSON Array格式编写字段body,如:
body: |
  [
    {
      "name": "hello"
    },
    {
      "name": "title"
    }
  ]
编写返回内容验证
编写gRPC API返回内容验证的方式与HTTP API基本相同。对与gRPC API来说,一切返回值都被视为map类型,被放入api testing特定的返回结构中:
expect:
  body: |
    {
      "data": [
        {
          "key": "hello",
          "value": "func() string"
        }
      ]
    }
api-testing为JSON比对编写了一套对比库,请参考此处compare
请注意,对于服务端流和双向流模式,服务器发送多条消息的情况下,此处的data字段内的填写的目标数组,需同时满足与待验证数组长度相,两个数组同一下标的内容完全相同。
gRPC API 的 verify 功能与 HTTP API 保持一致,此处不再赘述。
10 - gRPC TLS verification
If you want to enable gRPC TLS, you need to generate your certificates in the workspace, or you can use an absolute path
1. 生成私钥
openssl genrsa -out server.key 2048
2. 生成证书(会提示即可,不必填写)
openssl req -new -x509 -key server.key -out server.crt -days 36500
国家名字
Country Name (2 letter code) [AU]:CN
省份全名
State or Province Name (full name) [Some-State]:GuangDong
城市名
Locality Name (eg, city) []:Meizhou
组织名
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Xuexiangban
组织单位名
Organizational Unit Name (eg, section) []:go
服务器or用户的名字
Common Name (e.g. server FQDN or YOUR name) []:kuangstudy
邮箱地址
Email Address []:24736743@qq.com
3.生成csr
openssl req -new -key server.key -out server.csr
4.配置openssl.cfg
1) 查找openssl在服务器的安装目录并且找到openssl.cnf
2) 编辑[ CA_default ] ,打开 copy_extensions = copy #取消注释
3) 找到[ req ],打开 req_extensions = v3.req #取消注释
4) 找到[ v3_req ],添加字段 subjectAltName = @alt_names
5) 添加新的标签在最底部 [ alt_names ]和标签字段
DNS.1 = localhost
5.生成本地私钥test.key
openssl genpkey -algorithm RSA -out test.key
6.根据私钥生成csr请求文件test.csr
openssl req -new -nodes -key test.key -out test.csr -days 3650 \
-subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" \
-config ./openssl.cnf -extensions v3_req
7.生成ca证书 pem
openssl x509 -req -days 365 -in test.csr \
-out test.pem -CA server.crt -CAkey server.key \
-CAcreateserial -extfile ./openssl.cnf -extensions v3_req
11 - 插件
atest 会把非核心、可扩展的功能以插件(extension)的形式实现。下面介绍有哪些插件,以及如何使用:
在不同的系统中,插件有着不同的表述,例如:extension、plugin 等。
| 类型 | 名称 | 描述 | 
|---|---|---|
| 存储 | orm | 保存数据到关系型数据库中,例如:MySQL、Postgres 等 | 
| 存储 | mongodb | 保存数据到 MongDB 中 | 
| 存储 | etcd | 保存数据到 Etcd 数据库中 | 
| 存储 | iotdb | 支持通过 Web UI 查询 IoTDB 数据 | 
| 存储 | Cassandra | 支持通过 Web UI 查询 Cassandra 数据 | 
| 存储 | Elasticsearch | 支持通过 Web UI 查询 Elasticsearch 数据 | 
| 存储 | redis | |
| 存储 | s3 | 保存数据到对象存储中 | 
| 存储 | git | 保存数据到 Git 仓库中 | 
| 数据 | Swagger | 
atest也是唯一支持如此丰富的存储的接口开发、测试的开源工具。
下载插件
我们建议通过如下的命令来下载插件:
atest extension orm
上面的命令,会识别当前的操作系统,自动下载最新版本的插件。当然,用户可以通过自行编译、手动下载的方式获取插件二进制文件。
atest 可以从任意支持 OCI 的镜像仓库中(命令参数说明中给出了支持的镜像服务地址)下载插件,也可以指定下载超时时间:
atest extension orm --registry ghcr.io --timeout 2ms
想要下载其他类型的插件的话,可以使用下面的命令:
atest extension --kind data swagger