网易首页 > 网易数码 > 正文

七牛如何做HTTP服务测试

0
分享至

4月26日,为期两天的开发者盛会Gopher China完美闭幕。在此次会议上,开发者们除了见到Go语言创始人Robert Griesemer,穿上了神气的“中国版土拨鼠T-shirt”外,还收获到了满满的技术干货。作为Go语言在国内的践行者,七牛云CEO许式伟在Gopher China上为开发者们分享了七牛如何做HTTP服务测试的经验。

基于HTTP协议来提供服务的好处是显然的。除了HTTP服务有很多现成的客户端、服务端框架可以直接使用外,在HTTP服务的调试、测试等工程领域都有现成的相关工具支撑。七牛大量的服务都基于HTTP,所以需要思考如何更有效地进行HTTP服务的测试。

七牛早期HTTP服务的测试方法是先写好服务端,然后写一个客户端SDK,再基于这个客户端SDK写测试案例。这种方法多多少少会遇到一些问题。首先,客户端SDK的修改可能会导致测试案例编不过。其次,客户端SDK通常是使用方友好,而不是测试方友好。服务端开发过程和客户端SDK耦合容易过早地陷入“客户端SDK如何抽象更合理”的细节,而不能专注于测试服务逻辑本身。我的核心诉求是服务端开发过程和客户端开发过程解耦,因为网络协议定好了以后,整个系统原则上就可以编写测试案例,而不用等客户端SDK的完成。

不写客户端SDK而直接做HTTP测试,一个直观的思路是直接基于http.Client类来写测试案例。这种方式的问题是代码比较冗长,而且它的业务逻辑表达不直观,很难一眼看出这句话想干什么。虽然可以写一些辅助函数来改观,但是会逐渐有写测试专用SDK的倾向。这种写法看起来也不是很可取,毕竟为测试写一个专门的SDK,看起来成本有些高了。

七牛当前的做法是引入一种httptestDSL文法。这是七牛为测试写的领域专用语言。这个语言的文法大概在2012年就已经被加入到七牛的代码库,后来有个同事根据这个DSL文法写了第一版本qiniutest程序。在决定推广用这个DSL来进行测试的过程中,我们对DSL不断地进行了调整和加强。虽然总体思路没有变化,但最终定稿的DSL与最初版本有较大的差异。目前来说,我已经可以十分确定地说,这个DSL可以满足90%以上的测试需求。现在所有新写的模块全部基于这套测试案例进行测试,它被推荐做为七牛内部的首选测试方案。

上图是这套DSL的“hello world”程序。执行预期:下载www.qiniu.com首页,要求返回的HTTP状态码为200。如果返回非200,测试失败;否则则测试通过,打印返回包的正文内容(resp.body变量)。打印resp.body通常是调试需要,而不是测试需要。自动化测试是不需要去打印什么的。

我们再看该DSL的一个“quick start(快速入门)”样例。以#开始的内容是程序的注释部分。这里有一个很长很长的注释,描述了一个基本的HTTP请求测试的构成。后面我们会对这部分内容进行详细展开,这里暂时跳过。这段代码的第一句话是定义了一个auth别名叫qiniutest,这只是为了让后面具体的HTTP请求中授权语句更简短。紧接着是发起一个POST请求,创建一个内容为 {"a":"value1", "b": 1}的对象,并将返回的对象id赋值给一个名为id1的变量。后面我们会详细解释这个赋值过程是如何进行的。接着我们发起一个获取对象内容的GET请求,需要注意的是我们GET的URL中引用了id1的值,这意味着我们不是要取别的对象的内容,而是取刚刚我们创建成功的对象的内容,并且我们期望返回的对象内容和刚才我们POST上去的一样,也是{"a":"value1", "b": 1}。这就是一个最基础的HTTP测试,它创建一个对象,确认创建成功,并且尝试去取会这个对象,并确认内容与预期的一致。这里上下两个请求是通过一个变量来关联的。

对我们的DSL文法有了一个大概的印象后,我们开始解剖这套DSL。我们先来看看它的语法结构。首先我们这套httptest DSL基于命令行文法:

command switch1 switch2 …arg1 arg2…

整个命令行先是一个命令,然后紧接着是一个个开关(可选),最后是一个个的命令参数。和大家熟悉的命令行比如LinuxShell一样,它也会有一些参数需要转义,如果参数包含空格或其他特殊字符,则可以用'\'转义,如'\'表示'','\t'表示TAB等。另外我们也支持用'…'或者"…"去引用一个比较复杂的文本作为参数,比如json格式的多行文本。同LinuxShell类似,'…'里面的内容没有转义,'\'就是'\','\t'就是'\t',而不是 TAB。而"…"则支持转义。

和Linux Shell 不同的是,我们的httptest DSL虽然基于命令行文法,但是它的每一个参数都是有类型的,也就是说这个语言有类型系统,而不像Linux Shell只有字符串。我们的httptest DSL支持切仅支持所有json支持的数据类型,包括:

string (如:"a"、application/json),在不引起歧义的情况下,可以省略双引号

number (如:3.14159)

boolean (如:true、false)

array (如:["a", 200, {"b": 2}])

dictionary/object (如:{"a": 1, "b": 2})

另外,我们的httptest DSL也有子命令的概念,它相当于一个函数,可以返回任意类型的数据。比如 `qiniu f2weae23e6c9f jg35fae526kbce` 返回一个auth object,这用字符串无法表达。

理解了httptest DSL后,我们来看看如何表达一个http请求。它的基本形式如下:

req<http-method><url>

header<key1><val11><val12>

header<key2><val21><val22>

auth<authorization>

body<content-type><body-data>

第一句是req指令,带两个参数:一个是http method,即http请求的方法,另一个是要请求的URL。,接着是一个个自定义的header(可选),每个header指令后面跟一个key(键)和一个或多个value(值)。然后是一个可选的auth指令,用来指示这个请求的授权方式。如果没有auth语句,那么这个http请求是匿名的,否则这就是一个带授权的请求。最后一句是body指令,顾名思义它用来指定http请求的正文。body指令也是两个参数,一个是content-type(内容格式),另一个是body-data(请求正文)。

这样说比较抽象,我们看下实际的例子:

无授权的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}'

也可以简写成:

无授权的GET请求:

gethttp://www.qiniu.com/

带授权的Post请求:

posthttp://foo.com/objects

auth'qiniu f2weae23e6c9f jg35fae526kbce'

json'{"a": "hello1", "b":2}'

发起了http请求后,我们就可以收到http返回包并匹配。http返回包匹配的基本形式如下:

ret<expected-status-code>

header<key1><expected-val11><expected-val12>

header<key2><expected-val21><expected-val22>

body<expected-content-type><expected-body-data>

我们先看ret指令。实际上,请求发出去的时间是在ret指令执行的时候。前面req、header、auth、body指令仅仅表达了http请求,但是如果没有调用ret指令,那么系统什么也没有发生。

ret指令可以不带参数。不带参数的ret指令,其含义是发起http请求,并将返回的http 返回包解析并存储到resp的变量中。而对于带参数的ret指令:

ret<expected-status-code>

它等价于:

ret

match<expected-status-code>$(resp.code)

这里我们引入了一个新的指令—— match指令:

七牛所有http返回包匹配的匹配文法,都可以用这个match来表达:

所以本质上来说,我们只需要一个不带参数的ret,加上match指令,就可以搞定所有的返回包匹配过程。这也是我们为什么说match指令是这套DSL中最核心的概念的原因。

和其他自动化测试框架类似,这套DSL也提供了断言文法。它类似于CppUnit或JUnit之类的测试框架提供assertEqual。具体如下:

equal<expected><source>

与match不同,<expected>、<source>中都不允许出现未绑定的变量

与match不同equal要求<expected>、<source>的值精确相等

equalSet<expected><source>

SET是指集合

与equal不同,equalSet要求<expected>、<source>都是array,并且对array的元素进行排序后两者精确相等

equalSet的典型使用场景是list类的API,比如列出一个目录下的所有文件,你可能预期这个目录下有哪些文件,但是不能预期他们会以什么样的次序返回

以上介绍基本上就是这套DSL最核心的内容了。内容非常精简,但满足了绝大部分测试场景的需求。下面我们谈谈最后一个话题:测试环境的参数化。

为了让测试案例更加通用,我们需要对测试依赖的环境进行参数化。比如,为了让测试脚本能够同时用于stage环境和product环境,我们需要把服务的Host信息参数化。另外,为了方便测试脚本入口,我们通常还需要把用户名/密码、AK/SK(公私钥对)等敏感性信息参数化,避免直接硬编码到测试案例中。

为了把服务器的Host信息(也就是服务器的位置)参数化,我们引入了host指令。例如:

host foo.com 127.0.0.1:8888

get http://foo.com/objects/a325gea2kgfd

authqiniutest

ret 200

json '{"a": "hello1", "b": 2}'

这样,后文所有出现请求foo.com地方,都会把请求发送到127.0.0.1:8888这样一个服务器地址。要想让脚本测试另一个测试服务器,那么我们只需要调整host语句,将127.0.0.1:8888调整成其他即可。

除了服务器Host需要参数化外,其他常见的参数化需求是Username/Password、AK/SK(公私钥对)等。AK/SK这样的信息非常敏感,如果在测试脚本里面硬编码这些信息,将非常不方便测试脚本的入库。一个典型的测试环境参数化后的测试脚本样例如下:

其中,env指令用于取环境变量对应的值(返回值类型是string),envdecode指令则是先取得环境变量对应的值,然后对值进行jsondecode得到相应的object/dictionary。有了$(env)这个对象(object),就可以通过它获得各种测试环境参数,比如 $(env.FooHost)、$(env.AK)、$(env.SK) 等。

写好了测试脚本后,在执行测试脚本之前,我们需要先配置测试环境:

exportQiniuTestEnv_stage='{

"FooHost":"192.168.1.10:8888",

"AK":"…",

"SK":"…"

}'

exportQiniuTestEnv_product='{

"FooHost":"foo.com",

"AK":"…",

"SK":"…"

}'

这样我们就可以执行测试脚本了:

测试 stage 环境:

QiniuTestEnv=stage qiniutest ./testfoo.qtf

测试 product 环境:

QiniuTestEnv=product qiniutest ./testfoo.qtf

测试是软件质量保障至关重要的一环。一个好的测试工具对提高开发效率的作用巨大。如果能够让开发人员的开发时间从一小时减少到半小时,日积月累你就会得到一个惊人的数据。七牛非常乐意去关注开发人员日常工作过程中的不爽和低效率,我们认为,任何开发效率提升相关的工作,其收益都是指数级的。这也是七牛团队所推崇的做事风格。

  
相关推荐
热点推荐
48岁女保姆哭诉:和雇主同吃同住,每月工资8000元,我却苦不堪言

48岁女保姆哭诉:和雇主同吃同住,每月工资8000元,我却苦不堪言

热心柚子姐姐
2026-01-08 15:44:15
NBA官宣周最佳:沃特森首次当选巴恩斯上榜 小卡阿夫迪亚等获提名

NBA官宣周最佳:沃特森首次当选巴恩斯上榜 小卡阿夫迪亚等获提名

罗说NBA
2026-01-13 06:28:57
老板大气!苏州一工厂给员工发年终奖,按工龄1年1000,上不封顶

老板大气!苏州一工厂给员工发年终奖,按工龄1年1000,上不封顶

火山诗话
2026-01-12 06:54:34
开拓者114-123尼克斯!无缘6连胜,布伦森26+6+8,杨瀚森定律诞生

开拓者114-123尼克斯!无缘6连胜,布伦森26+6+8,杨瀚森定律诞生

球场没跑道
2026-01-12 09:31:12
车主扎堆露财反驳黑子买不起保时捷才买小米言论!雷军:感谢认可

车主扎堆露财反驳黑子买不起保时捷才买小米言论!雷军:感谢认可

柴狗夫斯基
2026-01-12 11:18:03
欧洲终于硬起来了,向美国开出条件,特朗普不答应就考虑制裁美企

欧洲终于硬起来了,向美国开出条件,特朗普不答应就考虑制裁美企

兴史兴谈
2026-01-13 06:02:09
19:30,CCTV5直播!中国队vs泰国,打平=出线,改写U23亚洲杯历史

19:30,CCTV5直播!中国队vs泰国,打平=出线,改写U23亚洲杯历史

侃球熊弟
2026-01-13 03:03:30
日本人不爱运动却最长寿,且患癌率极低,医生:5个原因值得深思

日本人不爱运动却最长寿,且患癌率极低,医生:5个原因值得深思

坠入二次元的海洋
2026-01-13 06:01:13
开年A股风格分化:沪指跑输个股均值,超级大盘超八成收跌,中小盘股扛旗领跑

开年A股风格分化:沪指跑输个股均值,超级大盘超八成收跌,中小盘股扛旗领跑

财联社
2026-01-12 18:36:06
美记者爆料:德舰过台海遭中方电磁压制,电子设备全瘫痪只能盲航

美记者爆料:德舰过台海遭中方电磁压制,电子设备全瘫痪只能盲航

罗富强说
2026-01-12 17:12:01
1979年打越南时,我国至少有200个师,但为何让大批新兵上战场?

1979年打越南时,我国至少有200个师,但为何让大批新兵上战场?

鹤羽说个事
2025-12-23 11:46:00
合川呆呆妹真实名字曝光,紧急辟谣两件事,相亲对象后悔莫及!

合川呆呆妹真实名字曝光,紧急辟谣两件事,相亲对象后悔莫及!

有范又有料
2026-01-12 16:49:53
老道士揭秘:家中这三样东西消失,一定是被人借运了!千万要小心

老道士揭秘:家中这三样东西消失,一定是被人借运了!千万要小心

古怪奇谈录
2026-01-05 11:32:51
28岁,丰满圆润,顶级身材太汹涌了

28岁,丰满圆润,顶级身材太汹涌了

技巧君侃球
2025-12-14 23:49:18
大跳水!暴跌40%,又土又贵还开遍机场,中产的标配,卖不动了

大跳水!暴跌40%,又土又贵还开遍机场,中产的标配,卖不动了

毒sir财经
2025-11-16 23:08:08
从濒临破产到重返巅峰,戴尔科技创始人回归后做对了哪些关键抉择

从濒临破产到重返巅峰,戴尔科技创始人回归后做对了哪些关键抉择

千秋文化
2026-01-12 20:32:22
一夜醒来,伊朗被迫面临强敌,特朗普下定决心,百万部队背水一战

一夜醒来,伊朗被迫面临强敌,特朗普下定决心,百万部队背水一战

顾蔡卫
2026-01-13 05:36:39
贝嫂不忍了?儿子让她向儿媳道歉,她说:我们没有什么好抱歉的

贝嫂不忍了?儿子让她向儿媳道歉,她说:我们没有什么好抱歉的

小书生吃瓜
2026-01-12 17:28:00
从古至今,真正能赚大钱的生意就这4个。

从古至今,真正能赚大钱的生意就这4个。

流苏晚晴
2026-01-10 16:23:24
明抢5000万桶石油后,特朗普转头才发现:中国连一桶都不肯买了

明抢5000万桶石油后,特朗普转头才发现:中国连一桶都不肯买了

历史有些冷
2026-01-12 08:40:23
2026-01-13 07:16:49

头条要闻

"摇人杀猪"女孩一天涨粉150万:我捅了大娄子

头条要闻

"摇人杀猪"女孩一天涨粉150万:我捅了大娄子

体育要闻

一场安东尼奥式胜利,给中国足球带来惊喜

娱乐要闻

蔡少芬结婚18周年,与张晋过二人世界

财经要闻

倍轻松信披迷雾 实控人占用资金金额存疑

科技要闻

面对SpaceX疯狂“下饺子” 中国正面接招

汽车要闻

增配不加价 北京现代 第五代 胜达2026款上市

态度原创

手机
旅游
家居
健康
公开课

手机要闻

小米Air手机遗憾遭砍:完全对标iPhone Air!5.5mm支持实体卡+eSIM

旅游要闻

走进布哈拉古城

家居要闻

包络石木为生 野性舒适

血常规3项异常,是身体警报!

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版
×