--- title: "如何使用 Loadgen 来简化 HTTP API 请求的集成测试" date: 2023-10-20 lastmod: 2023-10-20 description: "本文介绍如何使用Loadgen简化HTTP服务的集成测试,通过示例展示单个请求测试、批量请求变量应用、响应断言验证及动态寄存器功能,最后优化配置以支持环境变量,提升测试灵活性。" tags: ["集成测试", "Loadgen"] summary: "引言 # 在编写 HTTP 服务的过程中,集成测试 1 是保证程序正确性的重要一环,如下图所示,其基本的流程就是不断向服务发起请求然后校验响应的状态和数据等: 为大量的 API 和用例编写测试是一件繁琐的工作,而 Loadgen 2 正是为了简化这一过程而设计的。 一个简单的测试 # 假定我们在 127.0.0.1:9100 端口监听了一个 Pizza 3 服务,现在我们通过如下配置来测试集合(collection)的创建: # loadgen.yml requests: - request: method: PUT url: http://127.0.0.1:9100/test_create_document 然后运行 loadgen -config loadgen.yml: $ loadgen -config loadgen.yml __ ___ _ ___ ___ __ __ / / /___\/_\ / \/ _ \ /__\/\ \ \ / / // ///_\\ / /\ / /_\//_\ / \/ / / /__/ \_// _ \/ /_// /_\\//__/ /\ / \____|___/\_/ \_/___,'\____/\__/\_\ \/ [LOADGEN] A http load generator and testing suite." --- ## 引言 在编写 HTTP 服务的过程中,**集成测试** [^1] 是保证程序正确性的重要一环,如下图所示,其基本的流程就是不断向服务发起请求然后校验响应的状态和数据等: {{% load-img "/img/blog/2023/integration-testing-with-loadgen/test-flow.svg" "测试流程" %}} 为大量的 API 和用例编写测试是一件繁琐的工作,而 **Loadgen** [^2] 正是为了简化这一过程而设计的。 ## 一个简单的测试 假定我们在 `127.0.0.1:9100` 端口监听了一个 **Pizza** [^3] 服务,现在我们通过如下配置来测试集合(collection)的创建: ```yaml # loadgen.yml requests: - request: method: PUT url: http://127.0.0.1:9100/test_create_document ``` 然后运行 `loadgen -config loadgen.yml`: ```text $ loadgen -config loadgen.yml __ ___ _ ___ ___ __ __ / / /___\/_\ / \/ _ \ /__\/\ \ \ / / // ///_\\ / /\ / /_\//_\ / \/ / / /__/ \_// _ \/ /_// /_\\//__/ /\ / \____|___/\_/ \_/___,'\____/\__/\_\ \/ [LOADGEN] A http load generator and testing suite. [INF] warmup started [INF] loadgen is up and running now. [INF] [PUT] http://127.0.0.1:9100/test_create_document - [INF] status: 200, error: , response: {"success":true,"collection":"test_create_document"} [INF] warmup finished ... ``` > 为了便于阅读,笔者对程序输出进行了简化,实际会略有区别 可以看到,Loadgen 实际上帮我们做了类似这样的操作: ```bash curl -XPUT http://127.0.0.1:9100/test_create_document ``` ## 一些简单的测试 上述示例中我们只测试了创建单个集合,但是实际情况下短时间内会有许多请求涌入,对于创建大量的集合我们又该如何测试呢? 这里就需要用到**变量** [^4] 的概念: ```yaml # loadgen.yml variables: - name: id type: sequence requests: - request: method: PUT url: http://127.0.0.1:9100/test_create_document_$[[id]] ``` 上述配置中,我们定义了一个名为 `id` 的变量,`sequence` 是一个特殊的类型——每次被读取时它的值会递增,因此 Loadgen 会不断发起类似这样的请求: ```bash curl -XPUT http://127.0.0.1:9100/test_create_document_0 curl -XPUT http://127.0.0.1:9100/test_create_document_1 curl -XPUT http://127.0.0.1:9100/test_create_document_2 ... ``` 在 Pizza 的日志中也记录了这些请求: ```text $ pizza ___ _____ __________ _ / _ \\_ \/ _ / _ / /_\ / /_)/ / /\/\// /\// / //_\\ / ___/\/ /_ / //\/ //\/ _ \ \/ \____/ /____/____/\_/ \_/ [PIZZA] The Next-Gen Real-Time Hybrid Search & AI-Native Innovation Engine. [INFO] Collection test_create_document_0 created [INFO] Collection test_create_document_1 created [INFO] Collection test_create_document_2 created ... ``` ## 不那么简单的测试 目前为止,我们只是不断的向一个服务“塞”大量的请求,但比起发起请求,我们常常更关心程序的响应是否符合预期,也就是说,响应需要满足我们定义的一些条件,这可以通过 Loadgen 提供的 **断言** [^5] 功能来实现: ```yaml # loadgen.yml variables: - name: id type: sequence runner: # 检查返回值是否正常 assert_error: true # 检查断言是否通过 assert_invalid: true requests: - request: method: PUT url: http://127.0.0.1:9100/test_create_document_$[[id]] assert: equals: # 注意,这里我们故意设置了一个“不正常”的值,以迫使断言失败 _ctx.response.body_json.success: false ``` 在上述配置中,我们启用了 Loadgen 的检查,然后定义了一个会失败的断言: - `equals` 会校验给定路径 `_ctx.response.body_json.success` 是否与期望值 `false` 相等 - `_ctx.response.body_json` 表示 JSON 格式的响应体 - `success` 表示响应体中该字段对应的值,可以用 `path.to.nested.key` 来访问嵌套的字段 也就是说,给定响应体 `{"success":true,"collection":"test_create_document"}`,Loadgen 会检查 `success` 的值是否为 `false`: ```text $ loadgen -debug -r 1 -d 3 -config loadgen.yml #0 request, PUT http://127.0.0.1:9100/test_create_document_$[[id]], assertion failed, skiping subsequent requests [WRN] '_ctx.response.body_json.success' is not equal to expected value: true #0 request, PUT http://127.0.0.1:9100/test_create_document_$[[id]], assertion failed, skiping subsequent requests [WRN] '_ctx.response.body_json.success' is not equal to expected value: true #0 request, PUT http://127.0.0.1:9100/test_create_document_$[[id]], assertion failed, skiping subsequent requests [WRN] '_ctx.response.body_json.success' is not equal to expected value: true #0 request, PUT http://127.0.0.1:9100/test_create_document_$[[id]], assertion failed, skiping subsequent requests [WRN] '_ctx.response.body_json.success' is not equal to expected value: true ``` > 上述命令我们使用了: > > - `-debug` 启用更详细的报错 > - `-r 1 -d 3` 减少发起的请求数(`1req/s` 持续 `3s`) > > 还有一个需要注意的细节是 `... is not equal to expected value: true`,这里报告的是 `success` 字段实际的值,而不是断言中定义的期望值。 可以看到,Loadgen 每次请求的断言都失败了,不过我们可以通过日志来快速定位出错的原因以便于调试。 ## 更进一步的测试 现在我们创建了大量的空集合,是时候向其中添加一些文档(document)了,但是,一个首要解决的问题是,每次测试创建的集合名称是带有 `$[[id]]` 这个变量的,我们如何知道应该向哪个集合上传数据呢?一个可靠的解决方案是借助 Loadgen 的**寄存器** [^6] 功能: ```yaml # loadgen.yml variables: - name: id type: sequence runner: assert_error: true assert_invalid: true requests: - request: method: PUT url: http://127.0.0.1:9100/test_create_document_$[[id]] assert: equals: _ctx.response.body_json.success: true register: # 把响应体的 collection 字段赋值给 $[[collection]] - collection: _ctx.response.body_json.collection - request: method: POST # 在上个请求创建的集合里添加一个文档 url: http://127.0.0.1:9100/$[[collection]]/_doc body: '{"hello": "world"}' assert: equals: _ctx.response.body_json.result: created ``` 上述示例中,我们利用动态注册的变量记录了每次测试创建的集合以便于后续请求使用。 ## 最后的优化 为了使我们的配置更加灵活和“便携”,我们可以用环境变量来替换一些硬编码的值: ```yaml # loadgen.yml variables: - name: id type: sequence runner: assert_error: true assert_invalid: true requests: - request: method: PUT # 读取 PIZZA_SERVER 这个环境变量 url: $[[env.PIZZA_SERVER]]/test_create_document_$[[id]] assert: equals: _ctx.response.body_json.success: true register: - collection: _ctx.response.body_json.collection - request: method: POST url: $[[env.PIZZA_SERVER]]/$[[collection]]/_doc body: '{"hello": "world"}' assert: equals: _ctx.response.body_json.result: created ``` 这样就可以通过: ```bash PIZZA_SERVER=http://127.0.0.1:9101 loadgen -config loadgen.yml ``` 在不同的 Pizza 服务上运行测试。 [^1]: [^2]: [^3]: [^4]: [^5]: [^6]: