項(xiàng)目組里面的e2e測(cè)試運(yùn)行多年,歷經(jīng)了經(jīng)常會(huì)出現(xiàn)各種莫名其妙的環(huán)境問(wèn)題、運(yùn)行變慢等問(wèn)題后,項(xiàng)目組終于決定引入API功能測(cè)試。同時(shí)可以在盡量保證測(cè)試覆蓋率的前提下把重復(fù)測(cè)試的e2e測(cè)試腳本清理掉,提高持續(xù)集成效率(策略參考測(cè)試金字塔)。
那么問(wèn)題來(lái)了,做API功能測(cè)試如何選擇工具勒?API功能測(cè)試可以通過(guò)soapUI或者postman等帶GUI的工具簡(jiǎn)單錄制腳本執(zhí)行,也可以通過(guò)開(kāi)源項(xiàng)目工具自己寫(xiě)代碼完成。根據(jù)項(xiàng)目的實(shí)際情況,這里我們選擇使用后者,便于定制和持續(xù)集成。
工具選擇
目前市面上比較流行的API測(cè)試開(kāi)源框架有很多。首先能夠想到的就是REST-assured。Rest-Assured 是一套由 Java 實(shí)現(xiàn)的 REST API 測(cè)試框架,它是一個(gè)輕量級(jí)的 REST API 客戶(hù)端,可以直接編寫(xiě)代碼向服務(wù)器端發(fā)起 HTTP 請(qǐng)求,并驗(yàn)證返回結(jié)果。官方的介紹是:
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.
打開(kāi)github提交記錄,發(fā)現(xiàn)這個(gè)框架最近還有人在持續(xù)提交代碼,說(shuō)明維護(hù)的還不錯(cuò),列為備選項(xiàng)目。
另外經(jīng)過(guò)各種途徑了解到目前還有一套非常流行的,由大神tj等人開(kāi)發(fā)的nodeJS測(cè)試框架supertest。這是一套脫胎于著名的superagent的API測(cè)試框架,官方的說(shuō)法是:
- Super-agent driven library for testing node.js HTTP servers using a fluent API
- HTTP assertions made easy via superagent.
稍微對(duì)比一下這兩個(gè)工具,從幾個(gè)方面來(lái)考慮取舍:
- 項(xiàng)目代碼基于Java,同時(shí)也有NodeJS代碼在里面,從環(huán)境上來(lái)講兩個(gè)工具都不需要再額外配置。這點(diǎn)兩者打個(gè)平手。
- 學(xué)習(xí)成本方面,兩個(gè)工具都可以方便的從網(wǎng)上搜出一大堆學(xué)習(xí)資料,而且官方給的資料也比較全。又是平手。
- 維護(hù)成本上講,supertest是基于動(dòng)態(tài)語(yǔ)言,不需要浪費(fèi)編譯時(shí)間;萬(wàn)一寫(xiě)錯(cuò)了代碼立馬改完立馬重新跑起來(lái)。而且官網(wǎng)上號(hào)稱(chēng)"SuperTest works with any test framework"可擴(kuò)展性貌似也比較強(qiáng)。
- 從可移植性上看,supertest由于使用nodeJS,理論上只要框架做的夠好,只要有node,就可以把同一套腳本丟到各種不同的地方運(yùn)行。
- 最后再對(duì)比下易用性。安裝方面,REST-assured通常會(huì)借助如maven、grade之類(lèi)的工具安裝,配置運(yùn)行環(huán)境比較麻煩。而superset只需要簡(jiǎn)單的一行npm install 命令安裝后即可使用??紤]到我比較懶,supertest完勝,就醬。
開(kāi)始入坑
開(kāi)始學(xué)習(xí)supertest。
首先打開(kāi)它的github,了解supertest幾個(gè)關(guān)鍵信息:
- 繼承了superagent所有的API和用法。
- 使用前需安裝node,然后用
npm install supertest --save-dev或者cnpm install supertest --save-dev安裝supertest。 - 和superagent一樣,需要通過(guò)調(diào)用
.end()執(zhí)行一個(gè)request請(qǐng)求。 - 調(diào)用
.expect()來(lái)做斷言,如果在里面填入數(shù)字,默認(rèn)是檢查http請(qǐng)求返回的狀態(tài)碼;
完了我們來(lái)分析下官方示例代碼,然后仿造它來(lái)擼一段代碼試試看。
var request = require('supertest');
var express = require('express');
var app = express();
這里的app目測(cè)只是用來(lái)做一個(gè)mock server,跟supertest有關(guān)的測(cè)試只有下面這部分
request(app)
.get('/user')
.expect('Content-Type', /json/)
.expect('Content-Length', '15')
.expect(200)
.end(function(err, res) {
if (err) throw err;
});
分析這段測(cè)試代碼,首先是用request(app)實(shí)例化一個(gè)server,然后是.expect()分別驗(yàn)證了response header里面的content-type,content-length和response的http status是否200. 這就是supertest的基本寫(xiě)法了。
小試牛刀
我們用全球最大的同性交友平臺(tái)github來(lái)做個(gè)實(shí)驗(yàn),設(shè)計(jì)一個(gè)判斷是否成功進(jìn)入首頁(yè)的用例。
準(zhǔn)備工作:使用你的chrome,打開(kāi)develop tools的Network標(biāo)簽,先看看進(jìn)入github首頁(yè)時(shí)上有哪些請(qǐng)求,記錄下進(jìn)入首頁(yè)的請(qǐng)求,找到這個(gè)請(qǐng)求的URL,Method等關(guān)鍵信息。

實(shí)施階段:我們?cè)匐S便打開(kāi)個(gè)vim,記事本什么的文本編輯工具擼一小段代碼試試刀:
var request = require('supertest')('https://github.com/');
request
.get('/')
.expect(2010)
.end(function(err, res) {
if (err) throw err;
});
保存下來(lái),命名個(gè)test.js什么的,然后運(yùn)行它
node test.js
然后你發(fā)現(xiàn)得到這個(gè)提示異常的結(jié)果

這說(shuō)明我們的斷言成功了!把
.expect(2010)改成實(shí)際會(huì)返回的.expect(200)再試試看,沒(méi)有返回異常結(jié)果說(shuō)明測(cè)試通過(guò)了!
優(yōu)化一下:雖然測(cè)試成功了,但是這個(gè)測(cè)試結(jié)果的可讀性實(shí)在是有些令人不甚滿(mǎn)意,尤其是測(cè)試成功了連個(gè)提示都沒(méi)有。
于是我們考慮用官網(wǎng)例子中提到的測(cè)試框架Mocha來(lái)優(yōu)化下這個(gè)測(cè)試。
Mocha是一個(gè)優(yōu)秀的JavaScript測(cè)試框架,長(zhǎng)得跟Jasmine一個(gè)樣。官網(wǎng)上的介紹是:
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.
這個(gè)框架提供了各種style的測(cè)試報(bào)告。結(jié)合supertest使用,可以讓我們的API測(cè)試報(bào)告可視化上一個(gè)檔次。
順便可以加上個(gè)常用的post請(qǐng)求的測(cè)試:
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();
})
});
});
這個(gè)測(cè)試比起剛才的版本更加具有可讀性,借助Mocha框架,每段測(cè)試之前都有一個(gè)描述信息,一看就知道你這段代碼在測(cè)試什么。
其中before()是Mocha提供的hook,相當(dāng)于beforeAll,會(huì)在所有測(cè)試前執(zhí)行。其他hook還有會(huì)在所有測(cè)試執(zhí)行之后執(zhí)行的after(),會(huì)在每個(gè)測(cè)試前都執(zhí)行一遍的beforeEach()和會(huì)在每一個(gè)測(cè)試執(zhí)行之后都執(zhí)行一遍的afterEach()。hook用在清理測(cè)試數(shù)據(jù)方面會(huì)非常方便。
然后describe()描述了是測(cè)試的是什么東西:
describe('描述測(cè)試對(duì)象',function(){
//測(cè)試用例
})
而describe()里面的it()則描述了具體的測(cè)試用例:
it('描述測(cè)試用例', function(done){
//測(cè)試用例實(shí)現(xiàn)
done();
})
done()是Mocha提供的回調(diào)方法,如果沒(méi)有done()的話(huà)Javascript回一直等待回調(diào)致超時(shí)。順帶提一下Mocha的默認(rèn)超時(shí)時(shí)間是2秒,所以在describe的下面加上this.timeout(10000);把超時(shí)時(shí)間重新設(shè)置為10秒。
需要注意的是在雖然使用Mocha的時(shí)候可以忽略superset的.end(),而直接在.expect()添加done參數(shù),例如.expect(200,done)。但是如果使用了.end()的寫(xiě)法的話(huà),仍然需要在.end()塊兒中調(diào)用done()。
最后個(gè)用例中的.send('value=lala')是post的request body,通過(guò).type()來(lái)指定類(lèi)型。.type()在缺省狀態(tài)下默認(rèn)是JSON(詳見(jiàn)superagent源代碼),本例中使用的是form類(lèi)型。 當(dāng)然,也可以不用send()而是選擇直接在post的url中加上參數(shù)request.post('/signup_check/username?value=lala'),但是如果要參數(shù)化的話(huà),還是推薦用.send()。
Mocha還提供了watch功能,使用帶參數(shù)的命令mocha -w 測(cè)試腳本.js來(lái)監(jiān)視測(cè)試腳本,當(dāng)腳本有變化的時(shí)候Mocha會(huì)自動(dòng)運(yùn)行腳本。
測(cè)試結(jié)果如下:

更新最后一個(gè)用例中的.expect(404)為.expect(403),測(cè)試通過(guò)。

現(xiàn)在不管是測(cè)試代碼的可讀性還是測(cè)試報(bào)告的可讀性,都比之前強(qiáng)多了。而且還可以使用--reporter參數(shù)讓測(cè)試報(bào)告變成各種形狀,比如

查漏補(bǔ)缺:總算是解決了代碼可讀性和測(cè)試報(bào)告的問(wèn)題。再回過(guò)頭來(lái)看看整個(gè)demo,突然發(fā)現(xiàn)調(diào)研了這么半天,竟然忽略了在很多業(yè)務(wù)場(chǎng)景中,調(diào)用API需要驗(yàn)證用戶(hù)是否登錄的問(wèn)題。換句話(huà)說(shuō),需要在不同的http請(qǐng)求中保持cookie。
幸好supertest提供了這個(gè)解決方案,使用supertest的agent功能來(lái)解決這個(gè)問(wèn)題。
var request = require('superset')
describe('測(cè)試cookie', function(){
var agent = request.agent('待測(cè)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);
})
})
可以看到第一個(gè)用例是測(cè)試cookie=hey,而到了第二個(gè)測(cè)試?yán)锩?,由于被測(cè)實(shí)例由單純的"request"變成了"request.agent()",所以cookie “hey”被agent帶入到了第二個(gè)用例中,當(dāng)訪(fǎng)問(wèn)"/return"的時(shí)候不用再重新set cookies了。
另外我們也可以通過(guò)在每次請(qǐng)求前去set cookie的方法達(dá)到同樣的效果。
.set('Cookie', 'a cookie string')
最后如果是要測(cè)試授權(quán)資源的話(huà),superagent也提供了.auth()方法去獲取授權(quán)。
request .get('http://local')
.auth('tobi', 'learnboost') .
end(callback);
現(xiàn)在看上去調(diào)研工作算是差不多了,能夠滿(mǎn)足大部分的測(cè)試場(chǎng)景。接下來(lái)只需要再設(shè)計(jì)下測(cè)試代碼結(jié)構(gòu),抽象下公共組件,做下參數(shù)化,分離下測(cè)試數(shù)據(jù)就搞定了??墒羌?xì)想下,如果需要寫(xiě)了一大堆測(cè)試的話(huà),難道要挨個(gè)去執(zhí)行mocha xxx腳本的命令來(lái)跑測(cè)試?
還好項(xiàng)組目已經(jīng)在用grunt構(gòu)建工具。谷歌一下發(fā)現(xiàn)有一個(gè)grunt插件“grunt-mocha-test”貌似挺不錯(cuò)的。按照它的說(shuō)明文檔,只需要在grunt配置文件里面加上一段

其中reporter就是制定報(bào)告的格式, src就是需要執(zhí)行的腳本的路徑,
*.js指定執(zhí)行全部js格式的文件。
最后再注冊(cè)一個(gè)grunt命令,比如:
grunt.registerTask('apitest', 'mochaTest');
就能簡(jiǎn)單的在命令行中使用
grunt apitest
來(lái)執(zhí)行所有的測(cè)試文件了。這樣也可以方便的在Jenkins中配置一個(gè)新的測(cè)試任務(wù),加入持續(xù)集成。
至此,工具選型全部完成,核心是supertest,包裝是mocha,執(zhí)行用grunt,收工。
總結(jié)一下
總結(jié)一下,在工具選型的時(shí)候,建議考慮這些方面:
- 結(jié)合項(xiàng)目技術(shù)棧使用
- 新工具學(xué)習(xí)成本、維護(hù)成本、可擴(kuò)展性
- 是否可以簡(jiǎn)單實(shí)現(xiàn)代碼滿(mǎn)足所有業(yè)務(wù)場(chǎng)景,比如非REST風(fēng)格的API,或者一些特殊場(chǎng)景
- 代碼易讀,測(cè)試報(bào)告可視化
- 腳本執(zhí)行簡(jiǎn)單