加载中...

本篇要回答的问题:如何在不依赖客户端 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(不引起歧义时可省双引号)、numberbooleanarrayobject/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 这样的统一原语,你的测试案例能精简掉多少重复代码?

公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关