目组里面的 e2e 测试运行多年,历经了经常会出现各种莫名其妙的环境问题、运行变慢等问题后,项目组终于决定引入 API 功能测试。同时可以在尽量保证测试覆盖率的前提下把重复测试的 e2e 测试脚本清理掉,提高持续集成效率 (策略参考测试金字塔)。
那么问题来了,做 API 功能测试如何选择工具勒?API 功能测试可以通过 soapUI 或者 postman 等带 GUI 的工具简单录制脚本执行,也可以通过开源项目工具自己写代码完成。根据项目的实际情况,这里我们选择使用后者,便于定制和持续集成。
目前市面上比较流行的 API 测试开源框架有很多。首先能够想到的就是REST-assured。Rest-Assured 是一套由 Java 实现的 REST API 测试框架,它是一个轻量级的 REST API 客户端,可以直接编写代码向服务器端发起 HTTP 请求,并验证返回结果。官方的介绍是:
Testing and validation of REST services in Java is harder than in dynamic languages such as Ruby and Groovy. REST Assured brings the simplicity of using these languages into the Java domain.
打开 github 提交记录,发现这个框架最近还有人在持续提交代码,说明维护的还不错,列为备选项目。
另外经过各种途径了解到目前还有一套非常流行的,由大神 tj 等人开发的 nodeJS 测试框架supertest。这是一套脱胎于著名的superagent的 API 测试框架,官方的说法是:
Super-agent driven library for testing node.js HTTP servers using a fluent API
HTTP assertions made easy via superagent.
稍微对比一下这两个工具,从几个方面来考虑取舍:
开始学习 supertest。
首先打开它的github,了解 supertest 几个关键信息:
npm install supertest --save-dev
或者cnpm install supertest --save-dev
安装 supertest。.end()
执行一个 request 请求。.expect()
来做断言,如果在里面填入数字,默认是检查 http 请求返回的状态码;完了我们来分析下官方示例代码,然后仿造它来撸一段代码试试看。
var request = require('supertest');
var express = require('express');
var app = express();
这里的 app 目测只是用来做一个 mock server,跟 supertest 有关的测试只有下面这部分
request(app)
.get('/user')
.expect('Content-Type', /json/)
.expect('Content-Length', '15')
.expect(200)
.end(function(err, res) {
if (err) throw err;
});
分析这段测试代码,首先是用request(app)
实例化一个 server,然后是.expect()
分别验证了 response header 里面的 content-type,content-length 和 response 的 http status 是否 200. 这就是 supertest 的基本写法了。
我们用全球最大的同性交友平台 github 来做个实验,设计一个判断是否成功进入首页的用例。
准备工作:使用你的 chrome,打开 develop tools 的 Network 标签,先看看进入 github 首页时上有哪些请求,记录下进入首页的请求,找到这个请求的 URL,Method 等关键信息。
实施阶段:我们再随便打开个 vim,记事本什么的文本编辑工具撸一小段代码试试刀:
var request = require('supertest')('https://github.com/');
request
.get('/')
.expect(2010)
.end(function(err, res) {
if (err) throw err;
});
保存下来,命名个 test.js 什么的,然后运行它
node test.js
然后你发现得到这个提示异常的结果
这说明我们的断言成功了!把.expect(2010)
改成实际会返回的.expect(200)
再试试看,没有返回异常结果说明测试通过了!
优化一下:虽然测试成功了,但是这个测试结果的可读性实在是有些令人不甚满意,尤其是测试成功了连个提示都没有。
于是我们考虑用官网例子中提到的测试框架 Mocha 来优化下这个测试。
Mocha是一个优秀的 JavaScript 测试框架,长得跟Jasmine一个样。官网上的介绍是:
Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases. Hosted on GitHub.
这个框架提供了各种 style 的测试报告。结合 supertest 使用,可以让我们的 API 测试报告可视化上一个档次。
顺便可以加上个常用的 post 请求的测试:
var request = require('supertest')('https://github.com');
describe('Github home page',function(){
this.timeout(10000);
before('must be on home page',function(done){
request.get('/')
.expect(200,done);
});
it('could be navigated to register page',function(done){
request.get('/join')
.expect(200,done);
});
it('will refuse the request if username has been taken',function(done){
request.post('/signup_check/username')
.type('form')
.send('value=lala')
.expect(404)
.end(function(err,res){
if (err) return done(err)
done();
})
});
});
这个测试比起刚才的版本更加具有可读性,借助 Mocha 框架,每段测试之前都有一个描述信息,一看就知道你这段代码在测试什么。
其中before()
是 Mocha 提供的 hook,相当于 beforeAll,会在所有测试前执行。其他 hook 还有会在所有测试执行之后执行的after()
,会在每个测试前都执行一遍的beforeEach()
和会在每一个测试执行之后都执行一遍的afterEach()
。hook 用在清理测试数据方面会非常方便。
然后describe()
描述了是测试的是什么东西:
describe('描述测试对象',function(){
//测试用例
})
而`describe()`里面的`it()`则描述了具体的测试用例:
it('描述测试用例', function(done){
//测试用例实现
done();
})
done()
是 Mocha 提供的回调方法,如果没有done()
的话 Javascript 回一直等待回调致超时。顺带提一下 Mocha 的默认超时时间是 2 秒,所以在 describe 的下面加上this.timeout(10000);
把超时时间重新设置为 10 秒。
需要注意的是在虽然使用 Mocha 的时候可以忽略 superset 的.end()
,而直接在.expect()
添加 done 参数,例如.expect(200,done)
。但是如果使用了.end()
的写法的话,仍然需要在.end()
块儿中调用done()
。
最后个用例中的.send('value=lala')
是 post 的 request body,通过.type()
来指定类型。.type()
在缺省状态下默认是 JSON(详见superagent 源代码),本例中使用的是 form 类型。 当然,也可以不用 send() 而是选择直接在 post 的 url 中加上参数request.post('/signup_check/username?value=lala')
,但是如果要参数化的话,还是推荐用.send()
。
Mocha 还提供了 watch 功能,使用带参数的命令mocha -w 测试脚本.js
来监视测试脚本,当脚本有变化的时候 Mocha 会自动运行脚本。
测试结果如下:
更新最后一个用例中的.expect(404)
为.expect(403)
,测试通过。
现在不管是测试代码的可读性还是测试报告的可读性,都比之前强多了。而且还可以使用--reporter
参数让测试报告变成各种形状,比如
查漏补缺:总算是解决了代码可读性和测试报告的问题。再回过头来看看整个 demo,突然发现调研了这么半天,竟然忽略了在很多业务场景中,调用 API 需要验证用户是否登录的问题。换句话说,需要在不同的 http 请求中保持 cookie。
幸好 supertest 提供了这个解决方案,使用 supertest 的 agent 功能来解决这个问题。
var request = require('superset')
describe('测试cookie', function(){
var agent = request.agent('待测server');
it('should save cookies', function(done){
agent
.get('/')
.expect('set-cookie', 'cookie=hey; Path=/', done);
})
it('should send cookies', function(done){
agent
.get('/return')
.expect('hey', done);
})
})
可以看到第一个用例是测试cookie=hey
,而到了第二个测试里面,由于被测实例由单纯的"request"变成了"request.agent()",所以 cookie “hey” 被 agent 带入到了第二个用例中,当访问"/return"的时候不用再重新 set cookies 了。
另外我们也可以通过在每次请求前去 set cookie 的方法达到同样的效果。
.set('Cookie', 'a cookie string')
最后如果是要测试授权资源的话,superagent 也提供了.auth()
方法去获取授权。
request .get('http://local')
.auth('tobi', 'learnboost') .
end(callback);
现在看上去调研工作算是差不多了,能够满足大部分的测试场景。接下来只需要再设计下测试代码结构,抽象下公共组件,做下参数化,分离下测试数据就搞定了。可是细想下,如果需要写了一大堆测试的话,难道要挨个去执行 mocha xxx 脚本的命令来跑测试?
还好项组目已经在用grunt 构建工具。谷歌一下发现有一个 grunt 插件 “grunt-mocha-test” 貌似挺不错的。按照它的说明文档,只需要在 grunt 配置文件里面加上一段
其中 reporter 就是制定报告的格式, src 就是需要执行的脚本的路径,*.js
指定执行全部 js 格式的文件。
最后再注册一个 grunt 命令,比如:
grunt.registerTask('apitest', 'mochaTest');
就能简单的在命令行中使用
grunt apitest
来执行所有的测试文件了。这样也可以方便的在 Jenkins 中配置一个新的测试任务,加入持续集成。
至此,工具选型全部完成,核心是 supertest,包装是 mocha,执行用 grunt,收工。
总结一下,在工具选型的时候,建议考虑这些方面: