本篇要回答的问题:如何在不依赖客户端 SDK 的前提下,高效地为基于 HTTP 协议的服务编写测试?七牛给出的答案是一套专为 HTTP 测试设计的 DSL —— httptest。
本讲是一篇加餐,分享七牛在 HTTP 服务测试上的工程实践,不属于主线的架构推演。核心动机是把"服务端开发"与"客户端开发"解耦:网络协议一旦定好,系统原则上就该能写测试,而不必等客户端 SDK 成熟。文章从早期朴素做法的痛点讲起,逐层引出 httptest DSL 的文法、match 核心指令与测试环境参数化。
早期做法的问题:为什么不直接用 SDK 或 http.Client?
七牛最初的测试套路是"先写服务端 → 写客户端 SDK → 基于 SDK 写测试案例"。这条路有几处别扭。
| 方案 | 做法 | 问题 |
|---|---|---|
| 基于客户端 SDK 测试 | 用使用方友好的 SDK 写测试 | SDK 改动会让测试编不过;SDK 是使用方友好而非测试方友好;过早陷入"SDK 如何抽象"的细节,无法专注服务逻辑本身 |
直接用 http.Client |
裸调 HTTP 客户端类 | 代码冗长、业务意图不直观;加辅助函数后又会逐渐演变成写一套测试专用 SDK,成本过高 |
| httptest DSL(当前方案) | 一种为 HTTP 测试而生的领域专用语言 | 精简,已满足 90% 以上测试需求,是七牛内部首选 |
关键判断:核心诉求是对服务端开发与客户端开发解耦。协议即契约,契约定了就能测,不该被 SDK 的成熟度绑架。
项目已开源:github.com/qiniu/httptest(框架)、github.com/qiniu/qiniutest(带七牛账号与授权机制)。
httptest 基础文法
DSL 基于命令行文法,但与 Linux Shell 有一个本质区别:每个参数都有类型。
command switch1 switch2 … arg1 arg2 …
| 维度 | Linux Shell | httptest DSL |
|---|---|---|
| 结构 | 命令 + 开关 + 参数 | 同左 |
| 转义 | \ 前缀转义;'…' 不转义、"…" 转义 |
同左 |
| 参数类型 | 只有字符串 | 有类型系统,支持全部 JSON 类型 |
| 子命令 | 无 | 有,相当于函数,可返回任意类型(如 qiniu AK SK 返回一个 auth object) |
支持的类型即 JSON 的全部类型:string(不引起歧义时可省双引号)、number、boolean、array、object/dictionary。
如何表达一个 HTTP 请求
请求的基本形式由若干指令构成,且真正发请求的时刻是在 ret 指令执行时——req/header/auth/body 只是在"描述"请求。
| 指令 | 作用 | 参数 |
|---|---|---|
req <method> <url> |
声明请求方法与 URL | method、url |
header <key> <val…> |
自定义请求头(可选,可多条) | key + 一个或多个 value |
auth <authorization> |
授权方式(可选);无此句则为匿名请求 | 授权信息 |
body <content-type> <data> |
请求正文 | content-type、body-data |
常见请求还有简写形式:
| 完整写法 | 简写 |
|---|---|
req GET <url> |
get <url> |
req POST <url> |
post <url> |
body application/json '…' |
json '…' |
返回包匹配与 match 核心指令
收到返回包后用 ret + 匹配指令做断言:
ret <expected-status-code>
header <key> <expected-val…>
body <expected-content-type> <expected-body-data>
ret 的两种形态:
| 写法 | 含义 |
|---|---|
ret(无参) |
发起请求,把返回包解析存入 resp 变量 |
ret <status-code> |
等价于 ret + match <status-code> $(resp.code) |
关键洞见:本质上只要一个无参
ret加上match,就能搞定所有返回包匹配过程——这就是match被称为整套 DSL 最核心概念的原因。match还支持把返回值中的字段绑定到变量(如把对象 id 赋给id1),从而把上下游请求关联起来。
断言文法(类比 CppUnit / JUnit 的 assertEqual):
| 指令 | 语义 | 与 match 的区别 |
|---|---|---|
match |
模式匹配,允许未绑定变量(用于赋值绑定) | —— |
equal <expected> <source> |
要求两者精确相等 | 不允许出现未绑定变量 |
equalSet <expected> <source> |
两者均为 array,排序后比较 | 适合测 list 类 API(能预期有哪些文件,但不能预期返回顺序) |
测试环境的参数化
为让一套测试脚本能同时跑 stage 与 product 环境,需要把环境依赖抽出来。
| 指令 | 作用 |
|---|---|
host foo.com 127.0.0.1:8888 |
把对 foo.com 的请求统一改投到指定地址,换实例只需改这一句 |
env |
取环境变量的值(返回 string) |
envdecode |
取环境变量值后做 JSON decode,得到 object,可用 $(env.AK)、$(env.SK)、$(env.FooHost) 取参 |
敏感信息(AK/SK、用户名密码)通过环境变量注入,避免硬编码进而无法入库:
export QiniuTestEnv_stage='{"FooHost":"192.168.1.10:8888","AK":"…","SK":"…"}'
QiniuTestEnv=stage qiniutest ./testfoo.qtf
QiniuTestEnv=product qiniutest ./testfoo.qtf
总结
七牛用一套类型化的命令行 DSL 解决了 HTTP 服务测试的解耦与表达力问题:请求用 req/header/auth/body 声明、ret 触发、match 统一做断言、host/env 做环境参数化。文法极精简,却覆盖了 90% 以上的测试需求。
先把"是什么"回答清楚
| 概念 | 一句话说明 |
|---|---|
| httptest DSL | 为 HTTP 测试而生、带类型系统的命令行领域专用语言 |
| 解耦 | 让测试只依赖网络协议契约,而不依赖客户端 SDK |
ret |
真正发出请求的时刻;无参版把返回存入 resp |
match |
DSL 的灵魂指令,统一表达所有返回包匹配,并能绑定变量 |
equal / equalSet |
精确相等 / 集合相等(排序后比,适合 list API) |
| 环境参数化 | 用 host 换服务地址、用 env/envdecode 注入敏感配置 |
一句话速记
协议即契约——契约一定,就用一套带类型的命令行 DSL(
ret触发、match断言、env参数化)去测它,别让测试被客户端 SDK 绑架。
几条值得记住的判断
- 测试工具的收益是指数级的:把开发人员一次操作从一小时压到半小时,日积月累效果惊人。
- 关注开发者日常的"不爽与低效"非常值得——这是七牛推崇的做事风格。
- 自动化测试不需要向屏幕输出任何东西,输出
resp.body只是调试需要。
思考题
你所在团队的 HTTP 服务测试,是否也存在"测试代码强耦合客户端 SDK"的问题?如果把请求/断言抽象成一套像 match 这样的统一原语,你的测试案例能精简掉多少重复代码?
