這章我們學(xué)習(xí)如果編寫(xiě) Promise 的測(cè)試代碼
關(guān)于 ES6 Promises 的語(yǔ)法我們已經(jīng)學(xué)了一些, 我想大家應(yīng)該也能夠在實(shí)際項(xiàng)目中編寫(xiě) Promise 的 Demo 代碼了吧。
這時(shí),接下來(lái)你可能要苦惱該如何編寫(xiě) Promise 的測(cè)試代碼了。
那么讓我們先來(lái)學(xué)習(xí)下如何使用 Mocha 來(lái)對(duì) Promise 進(jìn)行基本的測(cè)試吧。
先聲明一下,這章中涉及的測(cè)試代碼都是運(yùn)行在 Node.js 環(huán)境下的。
本書(shū)中出現(xiàn)的示例代碼也都有相應(yīng)的測(cè)試代碼。 測(cè)試代碼可以參考 azu/promises-book 。
Mocha 是 Node.js 下的測(cè)試框架工具,在這里,我們并不打算對(duì) Mochahttp://mochajs.org/ 本身進(jìn)行詳細(xì)講解。對(duì) Mocha 感興趣的讀者可以自行學(xué)習(xí)。
Mocha可以自由選擇 BDD、TDD、exports 中的任意風(fēng)格,測(cè)試中用到的 Assert 方法也同樣可以跟任何其他類(lèi)庫(kù)組合使用。 也就是說(shuō),Mocha 本身只提供執(zhí)行測(cè)試時(shí)的框架,而其他部分則由使用者自己選擇。
這里我們選擇使用 Mocha,主要基于下面 3 點(diǎn)理由。
最后至于為什么說(shuō) 支持"Promise測(cè)試" ,這個(gè)我們?cè)诤竺嬖僦v。
要想在本章中使用 Mocha,我們需要先通過(guò) npm 來(lái)安裝 Mocha。
$ npm install -g mocha
另外,Assert 庫(kù)我們使用的是 Node.js 自帶的 assert 模塊,所以不需要額外安裝。
首先,讓我們?cè)囍帉?xiě)一個(gè)對(duì)傳統(tǒng)回調(diào)風(fēng)格的異步函數(shù)進(jìn)行測(cè)試的代碼。
如果想使用回調(diào)函數(shù)風(fēng)格來(lái)對(duì)一個(gè)異步處理進(jìn)行測(cè)試,使用 Mocha 的話代碼如下所示。
var assert = require('power-assert');
describe('Basic Test', function () {
context('When Callback(high-order function)', function () {
it('should use `done` for test', function (done) {
setTimeout(function () {
assert(true);
done();
}, 0);
});
});
context('When promise object', function () {
it('should use `done` for test?', function (done) {
var promise = Promise.resolve(1);
// このテストコードはある欠陥があります
promise.then(function (value) {
assert(value === 1);
done();
});
});
});
});
將這段代碼保存為 basic-test.js,之后就可以使用剛才安裝的 Mocha 的命令行工具進(jìn)行測(cè)試了。
$ mocha basic-test.js
Mocha 的 it 方法指定了 done 參數(shù),在 done() 函數(shù)被執(zhí)行之前, 該測(cè)試一直處于等待狀態(tài),這樣就可以對(duì)異步處理進(jìn)行測(cè)試。
Mocha 中的異步測(cè)試,將會(huì)按照下面的步驟執(zhí)行。
it("should use `done` for test", function (done) {
<1>
setTimeout(function () {
assert(true);
done();<2>
}, 0);
});
<1> 回調(diào)式的異步處理
<2> 調(diào)用 one 后測(cè)試結(jié)束
這也是一種非常常見(jiàn)的實(shí)現(xiàn)方式。
done 的 Promise 測(cè)試接下來(lái),讓我們看看如何使用 done 來(lái)進(jìn)行 Promise 測(cè)試。
it("should use `done` for test?", function (done) {
var promise = Promise.resolve(42);<1>
promise.then(function (value) {
assert(value === 42);<2>
done();
});
});
<1>創(chuàng)建名為 Fulfilled 的 promise 對(duì)象
<2>調(diào)用 done 后測(cè)試結(jié)束
Promise.resolve 用來(lái)返回 promise 對(duì)象, 返回的 promise 對(duì)象狀態(tài)為 FulFilled。 最后,通過(guò) .then 設(shè)置的回調(diào)函數(shù)也會(huì)被調(diào)用。
像專(zhuān)欄: Promise 只能進(jìn)行異步操作? 中已經(jīng)提到的那樣, promise 對(duì)象的調(diào)用總是異步進(jìn)行的,所以測(cè)試也同樣需要以異步調(diào)用的方式來(lái)編寫(xiě)。
但是,在前面的測(cè)試代碼中,在 assert 失敗的情況下就會(huì)出現(xiàn)問(wèn)題。
對(duì)異常promise測(cè)試
it("should use `done` for test?", function (done) {
var promise = Promise.resolve();
promise.then(function (value) {
assert(false);// => throw AssertionError
done();
});
});
在此次測(cè)試中 assert 失敗了,所以你可能認(rèn)為應(yīng)該拋出“測(cè)試失敗”的錯(cuò)誤, 而實(shí)際情況卻是測(cè)試并不會(huì)結(jié)束,直到超時(shí)。
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/7.png" alt="picture7" />
Figure 7. 由于測(cè)試不會(huì)結(jié)束,所以直到發(fā)生超時(shí)時(shí)間未知,一直會(huì)處于掛起狀態(tài)。
通常情況下,assert 失敗的時(shí)候,會(huì) throw 一個(gè) error, 測(cè)試框架會(huì)捕獲該 error,來(lái)判斷測(cè)試失敗。
但是,Promise 的情況下 .then 綁定的函數(shù)執(zhí)行時(shí)發(fā)生的 error 會(huì)被 Promise 捕獲,而測(cè)試框架則對(duì)此 error 將會(huì)一無(wú)所知。
我們來(lái)改善一下 assert 失敗的 promise 測(cè)試, 讓它能正確處理 assert 失敗時(shí)的測(cè)試結(jié)果。
測(cè)試正常失敗的示例
it("should use `done` for test?", function (done) {
var promise = Promise.resolve();
promise.then(function (value) {
assert(false);
}).then(done, done);
});
在上面測(cè)試正常失敗的示例中,為了確保 done 一定會(huì)被調(diào)用, 我們?cè)谧詈筇砑恿?.then(done, done); 語(yǔ)句。
assert 測(cè)試通過(guò)(成功)時(shí)會(huì)調(diào)用 done() ,而 assert 失敗時(shí)則調(diào)用 done(error) 。
這樣,我們就編寫(xiě)出了和 回調(diào)函數(shù)風(fēng)格的測(cè)試 相同的 Promise 測(cè)試。
但是,為了處理 assert 失敗的情況,我們需要額外添加 .then(done, done); 的代碼。 這就要求我們?cè)诰帉?xiě) Promise 測(cè)試時(shí)要格外小心,忘了加上上面語(yǔ)句的話,很可能就會(huì)寫(xiě)出一個(gè)永遠(yuǎn)不會(huì)返回直到超時(shí)的測(cè)試代碼。
在下一節(jié),讓我們接著學(xué)習(xí)一下最初提到的使用 Mocha 理由中的支持" Promises 測(cè)試"究竟是一種什么機(jī)制。
在這里,我們將會(huì)學(xué)習(xí)什么是 Mocha 支持的“對(duì) Promise 測(cè)試”。
官方網(wǎng)站 Asynchronous code 也記載了關(guān)于 Promise 測(cè)試的概要。
Alternately, instead of using the done() callback, you can return a promise. This is useful if the APIs you are testing return promises instead of taking callbacks:
這段話的意思是,在對(duì) Promise 進(jìn)行測(cè)試的時(shí)候,不使用 done() 這樣的回調(diào)風(fēng)格的代碼編寫(xiě)方式,而是返回一個(gè) promise 對(duì)象。
那么實(shí)際上代碼將會(huì)是什么樣的呢?這里我們來(lái)看個(gè)具體的例子應(yīng)該容易理解了。
var assert = require('power-assert');
describe('Promise Test', function () {
it('should return a promise object', function () {
var promise = Promise.resolve(1);
return promise.then(function (value) {
assert(value === 1);
});
});
});
這段代碼將前面 前面使用 done 的例子 按照 Mocha 的 Promise 測(cè)試方式進(jìn)行了重寫(xiě)。
修改的地方主要在以下兩點(diǎn):
done采用這種寫(xiě)法的話,當(dāng) assert 失敗的時(shí)候,測(cè)試本身自然也會(huì)失敗。
t("should be fail", function () {
return Promise.resolve().then(function () {
assert(false);// => 測(cè)試失敗
});
});
采用這種方法,就能從根本上省略諸如 .then(done, done); 這樣本質(zhì)上跟測(cè)試邏輯并無(wú)直接關(guān)系的代碼。
Mocha 已經(jīng)支持對(duì) Promises 的測(cè)試 | Web scratch 這篇(日語(yǔ))文章里也提到了關(guān)于 Mocha 對(duì) Promise 測(cè)試的支持。
因?yàn)?Mocha 提供了對(duì) Promise 的測(cè)試,所以我們會(huì)認(rèn)為按照 Mocha 的規(guī)則來(lái)寫(xiě)會(huì)比較好。 但是這種代碼可能會(huì)帶來(lái)意想不到的異常情況的發(fā)生。
比如對(duì)下面的 mayBeRejected() 函數(shù)的測(cè)試代碼,該函數(shù)返回一個(gè)當(dāng)滿足某一條件就變?yōu)?Rejected 的 promise 對(duì)象。
function mayBeRejected(){ <1>
return Promise.reject(new Error("woo"));
}
it("is bad pattern", function () {
return mayBeRejected().catch(function (error) {
assert(error.message === "woo");
});
});
<1>這個(gè)函數(shù)用來(lái)對(duì)返回的 promise 對(duì)象進(jìn)行測(cè)試
這個(gè)測(cè)試的目的包括以下兩點(diǎn):
mayBeRejected() 返回的 promise 對(duì)象如果變?yōu)?FulFilled 狀態(tài)的話
測(cè)試將會(huì)失敗
mayBeRejected() 返回的promise 對(duì)象如果變?yōu)?Rejected 狀態(tài)的話
在 assert 中對(duì) Error 對(duì)象進(jìn)行檢查
上面的測(cè)試代碼,當(dāng) promise 對(duì)象變?yōu)?Rejected 的時(shí)候,會(huì)調(diào)用在 onRejected 中注冊(cè)的函數(shù),從而沒(méi)有走正 promise 的處理常流程,測(cè)試會(huì)成功。
這段測(cè)試代碼的問(wèn)題在于當(dāng) mayBeRejected() 返回的是一個(gè) 為 FulFilled 狀態(tài)的 promise 對(duì)象時(shí),測(cè)試會(huì)一直成功。
function mayBeRejected(){ <1>
return Promise.resolve();
}
it("is bad pattern", function () {
return mayBeRejected().catch(function (error) {
assert(error.message === "woo");
});
});
<1> 返回的 promise 對(duì)象會(huì)變?yōu)?FulFilled
在這種情況下,由于在 catch 中注冊(cè)的 onRejected 函數(shù)并不會(huì)被調(diào)用,因此 assert 也不會(huì)被執(zhí)行,測(cè)試會(huì)一直通過(guò)(passed,成功)。
為了解決這個(gè)問(wèn)題,我們可以在 .catch 的前面加入一個(gè) .then 調(diào)用,可以理解為如果調(diào)用了 .then 的話,那么測(cè)試就需要失敗
function failTest() { <1>
throw new Error("Expected promise to be rejected but it was fulfilled");
}
function mayBeRejected(){
return Promise.resolve();
}
it("should bad pattern", function () {
return mayBeRejected().then(failTest).catch(function (error) {
assert.deepEqual(error.message === "woo");
});
});
<1>通過(guò) throw 來(lái)使測(cè)試失敗
但是,這種寫(xiě)法會(huì)像在前面 then or catch? 中已經(jīng)介紹的一樣, failTest 拋出的異常會(huì)被 catch 捕獲。
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/8.png" alt="picture8" />
Figure 8. Then Catch flow
程序的執(zhí)行流程為 then → catch,傳遞給 catch 的 Error 對(duì)象為 AssertionError 類(lèi)型 , 這并不是我們想要的東西。
也就是說(shuō),我們希望測(cè)試只能通過(guò)狀態(tài)會(huì)變?yōu)?onRejected 的 promise 對(duì)象, 如果 promise 對(duì)象狀態(tài)為 onFulfilled 狀態(tài)的話,那么該測(cè)試就會(huì)一直通過(guò)。
在編寫(xiě) 上面對(duì) Error 對(duì)象進(jìn)行測(cè)試的例子 時(shí), 怎么才能剔除那些會(huì)意外通過(guò)測(cè)試的情況呢?
最簡(jiǎn)單的方式就是像下面這樣,在測(cè)試代碼中判斷在各種 promise 對(duì)象的狀態(tài)下,應(yīng)進(jìn)行如何的操作。
變?yōu)?FulFilled 狀態(tài)的時(shí)候 測(cè)試會(huì)預(yù)期失敗
變?yōu)?Rejected 狀態(tài)的時(shí)候
使用 assert 進(jìn)行測(cè)試
也就是說(shuō),我們需要在測(cè)試代碼中明確指定在Fulfilled和Rejected這兩種狀態(tài)下,都需進(jìn)行什么樣的處理。
function mayBeRejected() {
return Promise.resolve();
}
it("catch -> then", function () {
// 變?yōu)镕ulFilled的時(shí)候測(cè)試失敗
return mayBeRejected().then(failTest, function (error) {
assert(error.message === "woo");
});
});
像這樣的話,就能在 promise 變?yōu)?FulFilled 的時(shí)候編寫(xiě)出失敗用的測(cè)試代碼了。
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/9.png" alt="picture9" />
Figure 9. Promise onRejected test
在 then or catch? 中我們已經(jīng)講過(guò),為了避免遺漏對(duì)錯(cuò)誤的處理, 與使用 .then(onFulfilled, onRejected) 這樣帶有二個(gè)參數(shù)的調(diào)用形式相比, 我們更推薦使用 then → catch 這樣的處理方式。
但是在編寫(xiě)測(cè)試代碼的時(shí)候,Promise 強(qiáng)大的錯(cuò)誤處理機(jī)制反而成了限制我們的障礙。 因此我們不得已采取了 .then(failTest, onRejected) 這種寫(xiě)法,明確指定 promise 在各種狀態(tài)下進(jìn)行何種的處理。
在本小節(jié)中我們對(duì)在使用 Mocha 進(jìn)行 Promise 測(cè)試時(shí)可能出現(xiàn)的一些意外情況進(jìn)行了介紹。
普通的代碼采用 then → catch 的流程的話比較容易理解
then 中處理
通過(guò)使用 .then(onFulfilled, onRejected) 這種形式的寫(xiě)法, 我們可以明確指定 promise 對(duì)象在變?yōu)?Fulfilled 或 Rejected 時(shí)如何進(jìn)行處理。
但是,由于需要顯示的指定 Rejected 時(shí)的測(cè)試處理, 像下面這樣的代碼看起來(lái)總是有一些讓人感到不太直觀的感覺(jué)。
promise.then(failTest, function(error){
// 使用assert對(duì)error進(jìn)行測(cè)試
});
在下一小節(jié),我們會(huì)介紹如何編寫(xiě) helper 函數(shù)以方便編寫(xiě) Promise 的測(cè)試代碼, 以及怎樣去編寫(xiě)更容易理解的測(cè)試代碼。
在繼續(xù)進(jìn)行說(shuō)明之前,我們先來(lái)定義一下什么是可控測(cè)試。在這里我們對(duì)可控測(cè)試的定義如下。
待測(cè)試的 promise 對(duì)象
如果編寫(xiě)預(yù)期為 Fulfilled 狀態(tài)的測(cè)試的話
如果一個(gè)測(cè)試能網(wǎng)羅上面的用例(Fail)項(xiàng),那么我們就稱(chēng)其為可控測(cè)試。
也就是說(shuō),一個(gè)測(cè)試用例應(yīng)該包括下面的測(cè)試內(nèi)容。
在前面使用了 .then 的代碼就是一個(gè)期望結(jié)果為 Rejected 的測(cè)試。
promise.then(failTest, function(error){
// 通過(guò)assert驗(yàn)證error對(duì)象
assert(error instanceof Error);
});
為了編寫(xiě)有效的測(cè)試代碼, 我們需要明確指定 promise 的狀態(tài) 為 Fulfilled or Rejected 的兩者之一。
但是由于 .then 的話在調(diào)用的時(shí)候可以省略參數(shù),有時(shí)候可能會(huì)忘記加入使測(cè)試失敗的條件。
因此,我們可以定義一個(gè) helper 函數(shù),用來(lái)明確定義 promise 期望的狀態(tài)。
筆者(原著者)創(chuàng)建了一個(gè)類(lèi)庫(kù) azu/promise-test-helper 以方便對(duì) Promise 進(jìn)行測(cè)試,本文中使用的是這個(gè)類(lèi)庫(kù)的簡(jiǎn)略版
首先我們創(chuàng)建一個(gè)名為 shouldRejected 的 helper 函數(shù),用來(lái)在剛才的 .then 的例子中,期待測(cè)試返回狀態(tài)為 onRejected 的結(jié)果的例子。
var assert = require('power-assert');
function shouldRejected(promise) {
return {
'catch': function (fn) {
return promise.then(function () {
throw new Error('Expected promise to be rejected but it was fulfilled');
}, function (reason) {
fn.call(promise, reason);
});
}
};
}
it('should be rejected', function () {
var promise = Promise.reject(new Error('human error'));
return shouldRejected(promise).catch(function (error) {
assert(error.message === 'human error');
});
});
shouldRejected 函數(shù)接收一個(gè) promise 對(duì)象作為參數(shù),并且返回一個(gè)帶有 catch 方法的對(duì)象。
在這個(gè) catch 中可以使用和 onRejected 里一樣的代碼,因此我們可以在 catch 使用基于 assertion 方法的測(cè)試代碼。
在 shouldRejected 外部,都是類(lèi)似如下、和普通的 promise 處理大同小異的代碼。
shouldRejected 方法catch 方法中編寫(xiě)進(jìn)行 onRejected 處理的代碼在使用 shouldRejected 函數(shù)的時(shí)候,如果是 Fulfilled 被調(diào)用了的話,則會(huì) throw 一個(gè)異常,測(cè)試也會(huì)失敗。
promise.then(failTest, function(error){
assert(error.message === 'human error');
});
// == 幾乎這兩段代碼是同樣的意思
shouldRejected(promise).catch(function (error) {
assert(error.message === 'human error');
});
使用 shouldRejected 這樣的 helper 函數(shù),測(cè)試代碼也會(huì)變得很直觀。
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/10.png" alt="picture10" />
Figure 10. Promise onRejected test
像上面一樣,我們也可以編寫(xiě)一個(gè)測(cè)試 promise 對(duì)象期待結(jié)果為 Fulfilled 的 shouldFulfilled helper 函數(shù)。
var assert = require('power-assert');
function shouldFulfilled(promise) {
return {
'then': function (fn) {
return promise.then(function (value) {
fn.call(promise, value);
}, function (reason) {
throw reason;
});
}
};
}
it('should be fulfilled', function () {
var promise = Promise.resolve('value');
return shouldFulfilled(promise).then(function (value) {
assert(value === 'value');
});
});
這和上面的 shouldRejected-test.js 結(jié)構(gòu)基本相同,只不過(guò)返回對(duì)象的 catch 方法變?yōu)榱?then ,promise.then 的兩個(gè)參數(shù)也調(diào)換了。
在本小節(jié)我們學(xué)習(xí)了如何編寫(xiě)針對(duì) Promise 特定狀態(tài)的測(cè)試代碼,以及如何使用便于測(cè)試的 helper 函數(shù)。
這里我們使用到的
shouldFulfilled和shouldRejected也可以在下面的類(lèi)庫(kù)中找到。 azu/promise-test-helper。
此外,本小節(jié)中的 helper 方法都是以 Mocha 對(duì) Promise 的支持 為前提的, 在 基于 done 的測(cè)試 中使用的話可能會(huì)比較麻煩。
是使用基于測(cè)試框架對(duì) Promis 的支持,還是使用基于類(lèi)似 done 這樣回調(diào)風(fēng)格的測(cè)試方式,每個(gè)人都可以自由的選擇,只是風(fēng)格問(wèn)題,我覺(jué)得倒沒(méi)必要去爭(zhēng)一個(gè)孰優(yōu)孰劣。
比如在 CoffeeScript 下進(jìn)行測(cè)試的話,由于 CoffeeScript 會(huì)隱式的使用 return 返回,所以使用 done 的話可能更容易理解一些。
對(duì) Promise 進(jìn)行測(cè)試比對(duì)通常的異步函數(shù)進(jìn)行測(cè)試坑更多,雖說(shuō)采取什么樣的測(cè)試方法是個(gè)人的自由,但是在同一項(xiàng)目中采取前后風(fēng)格一致的測(cè)試則是非常重要。