第1页
七牛如何做
HTTP服务测试?
许式伟 2015-4-18
第2页
HTTP服务测试
• 单元测试
– 某个独立子服务的测试
• 集成测试
– 整个集群对外业务API的测试
• Stage环境 • Product环境
第3页
怎么测?
• 七牛早期做法
– 实现服务逻辑(Service Implementation) – 实现客户端SDK(Client Implementation) – 基于客户端SDK写测试案例(Test Case)
• 问题
– 客户端SDK修改导致测试案例编不过 – 客户端SDK通常是使用方友好,而不是测试方友好 – 让服务端与客户端SDK耦合,容易过早陷入客户端SDK如何抽象
更合理的细节,而不能专注于测试服务逻辑本身
第4页
换个角度
• 直接基于协议测试呢?
– 比如,基于 http.Client 类直接写测试案例
• 问题
– 代码相对冗长 – 业务逻辑表达不直观
• 写一些辅助函数能够略为改观,不过会有逐步写测试专用SDK的倾向
第5页
七牛当前做法
• 引入 httptest DSL 文法 • 更接近基于 http.Client 写测试案例的思路
– 但努力让代码更直白体现测试用意
第6页
Hello, world!
第7页
Quick start
第8页
语法结构
• 基于命令行文法
command switch1 switch2 … arg1 arg2 …
• 转义
– 如果参数包含空格或其他特殊字符,则可以:
• 用 \ 转义
– 比如 '\ ' 表示 ' ' (空格),'\t' 表示 TAB 字符,等等
• 用 '...' 或 "..." 包含
– '...' 中不支持用 \ 转义,也不支持子命令,出现任何内容都当作普通字符 对待
第9页
语法结构
• 区别于Linux Shell的地方
– 参数类型不只是字符串,有完整类型系统(支持且仅支持所有 json的数据类型)
• string (如:"a"、application/json),在不引起歧义的情况下,可以 省略双引号
• number (如:3.14159) • boolean (如:true) • array (如:["a", 200, {"b": 2}]) • dictionary/object (如:{"a": 1, "b": 2})
– 子命令相当于函数,返回任意类型的数据
• 比如 `qiniu f2weae23e6c9f jg35fae526kbce` 返回一个 auth object,用字符串无法表达
第10页
http Request 的表达
req
header … header … auth body
第11页
样例
• 无授权的GET请求
req GET http://www.qiniu.com/
• 带授权的POST请求
req POST http://foo.com/objects auth `qiniu f2weae23e6c9f jg35fae526kbce` body application/json '{ "a": "hello1", "b": 2 }'
第12页
简写
• 无授权的GET请求 get http://www.qiniu.com/
• 带授权的POST请求
post http://foo.com/objects auth `qiniu f2weae23e6c9f jg35fae526kbce` json '{ "a": "hello1", "b": 2 }'
第13页
http Response 匹配
ret header … header … body
第14页
ret 指令
• ret
– 发起 http Request 请求,并将 http Response 存储到 $(resp) 变量中
• ret
– 等价于
ret match $(resp.code)
第15页
匹配(match)
• 这几乎是这套 DSL 中最核心的概念
– match
第16页
例子
• 例子1 match $(a.b) 1 match $(a.c) hello 结果 $(a) 的值为 {"b": 1, "c": "hello"}
• 例子2 match $(a.b) 1 match $(a.b) 1 #可以匹配,因为$(a.b)的值的确为1 match $(a.b) 2 #失败,1和2不相等
• 例子3 match '{"c": {"d": $(d)}}' '{"c": {"d": "hello", "e": "world"}, "f": 1}' 结果 $(d) 的值为 "hello"
第17页
理解 http Response 匹配
• ret
– 等价于
ret match $(resp.code)
• header …
– 等价于
match '[, , …]' $ (resp.header.)
• body
– 等价于
match '[]' $(resp.header.Content-Type) match $(resp.body)
第18页
断言
• equal
– 与 match 不同,, 中都不允许出现未绑 定的变量
– 与 match 不同,equal 要求 , 的值精确 相等
• equalSet
– Set 是指集合 – 与 equal 不同,equalSet 要求 , 都是
array,并且对 array 的元素进行排序后两者精确相等
第19页
例子
• 例子1
get http://foo.com/objects/a325gea2kgfd auth qiniutest ret 200 equal '{"a": "hello1", "b": 2}' $(resp.body) match '{"a": "hello1", "b": 2}' $(resp.body)
• 例子2
get http://foo.com/objects auth qiniutest ret 200 equalSet $(resp.body), '[
{"a": "hello1", "b": 2}, {"a": "world2", "b": 4} ]'
第20页
测试环境
• 如何让 stage 和 product 环境共享测试案例?
– 测试环境参数化 – 也方便测试脚本入库(不入库 User/Password、AK/SK 这种敏感
信息)
第21页
host 指令
• 服务地址参数化
host foo.com 127.0.0.1:8888
get http://foo.com/objects/a325gea2kgfd auth qiniutest ret 200 json '{"a": "hello1", "b": 2}'
第22页
Host、AK/SK 参数化
match $(testenv) `env QiniuTestEnv` match $(env) `envdecode QiniuTestEnv_$(testenv)` host foo.com $(env.FooHost) auth qiniutest `qiniu $(env.AK) $(env.SK)` post http://foo.com/objects auth qiniutest json '{"a": "hello1", "b": 2}' ret 200 json '{"id": $(id1)}' get http://foo.com/objects/$(id1) auth qiniutest ret 200 json '{"a": "hello1", "b": 2}'
第23页
env、envdecode 指令
• env
– 取环境变量 对应的值
• decode
– 将一个 json 字符串 decode 为对象(object)
• envdecode
– 取环境变量 对应的值,并且把它当做 json 字符串 decode 为一个对象(object)
– 等价于
match $(encodedval) `env ` decode $(encodedval)
第24页
配置测试环境
• 配置 stage、product 环境
export QiniuTestEnv_stage='{
"FooHost": "192.168.1.10:8888", "AK": "...", "SK": "..." }'
export QiniuTestEnv_product='{ "FooHost": "foo.com", "AK": "...", "SK": "..."
}'
第25页
执行测试案例
• 测试 stage 环境
QiniuTestEnv=stage qiniutest ./testfoo.qtf
• 测试 product 环境
QiniuTestEnv=product qiniutest ./testfoo.qtf
第26页
未覆盖的内容…
• echo/println – 打印变量内容(调试) • auth – 授权的详细解剖 • base64 – 对字符串 base64 encode/decode • case/setUp/tearDown – 如何定义多个案例 • …
第27页
Q & A
@许式伟
@七牛云存储