测试来带个节奏之API测试工具篇
项目组里面的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.
稍微对比一下这两个工具,从几个方面来考虑取舍:
- 项目代码基于Java,同时也有NodeJS代码在里面,从环境上来讲两个工具都不需要再额外配置。这点两者打个平手。
- 学习成本方面,两个工具都可以方便的从网上搜出一大堆学习资料,而且官方给的资料也比较全。又是平手。
- 维护成本上讲,supertest是基于动态语言,不需要浪费编译时间;万一写错了代码立马改完立马重新跑起来。而且官网上号称"SuperTest works with any test framework"可扩展性貌似也比较强。
- 从可移植性上看,supertest由于使用nodeJS,理论上只要框架做的够好,只要有node,就可以把同一套脚本丢到各种不同的地方运行。
- 最后再对比下易用性。安装方面,REST-assured通常会借助如maven、grade之类的工具安装,配置运行环境比较麻烦。而superset只需要简单的一行npm install 命令安装后即可使用。考虑到我比较懒,supertest完胜,就酱。
开始入坑
开始学习supertest。
首先打开它的github,了解supertest几个关键信息:
- 继承了superagent所有的API和用法。
- 使用前需安装node,然后用
npm install supertest --save-dev
或者cnpm install supertest --save-dev
安装supertest。 - 和superagent一样,需要通过调用
.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配置文件里面加上一段
grunt-mocha-test配置其中reporter就是制定报告的格式, src就是需要执行的脚本的路径,
*.js
指定执行全部js格式的文件。
最后再注册一个grunt命令,比如:
grunt.registerTask('apitest', 'mochaTest');
就能简单的在命令行中使用
grunt apitest
来执行所有的测试文件了。这样也可以方便的在Jenkins中配置一个新的测试任务,加入持续集成。
至此,工具选型全部完成,核心是supertest,包装是mocha,执行用grunt,收工。
总结一下
总结一下,在工具选型的时候,建议考虑这些方面:
- 结合项目技术栈使用
- 新工具学习成本、维护成本、可扩展性
- 是否可以简单实现代码满足所有业务场景,比如非REST风格的API,或者一些特殊场景
- 代码易读,测试报告可视化
- 脚本执行简单