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