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