鍍金池/ 教程/ HTML/ Promise 測試
Promise 測試
實(shí)戰(zhàn) Promise
用語集
前言
什么是 Promise
API Reference
Advanced

Promise 測試

這章我們學(xué)習(xí)如果編寫 Promise 的測試代碼

3.1. 基本測試

關(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 。

3.1.1. Mocha

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)理由。

  • 它是非常著名的測試框架
  • 支持基于 Node.js 和瀏覽器的測試
  • 支持"Promise測試"

最后至于為什么說 支持"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)行測試的代碼。

3.1.2. 回調(diào)函數(shù)風(fēng)格的測試

如果想使用回調(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)方式。

3.1.3. 使用 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ī)制。

3.2. Mocha 對(duì) Promise 的支持

在這里,我們將會(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
  • 返回結(jié)果為 promise 對(duì)象

采用這種寫法的話,當(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 測試的支持。

3.2.1. 意料之外(失敗的)的測試結(jié)果

因?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í)行流程為 thencatch,傳遞給 catch 的 Error 對(duì)象為 AssertionError 類型 , 這并不是我們想要的東西。

也就是說,我們希望測試只能通過狀態(tài)會(huì)變?yōu)?onRejected 的 promise 對(duì)象, 如果 promise 對(duì)象狀態(tài)為 onFulfilled 狀態(tài)的話,那么該測試就會(huì)一直通過。

3.2.2. 明確兩種狀態(tài),改善測試中的意外(異常)狀況

在編寫 上面對(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)用形式相比, 我們更推薦使用 thencatch 這樣的處理方式。

但是在編寫測試代碼的時(shí)候,Promise 強(qiáng)大的錯(cuò)誤處理機(jī)制反而成了限制我們的障礙。 因此我們不得已采取了 .then(failTest, onRejected) 這種寫法,明確指定 promise 在各種狀態(tài)下進(jìn)行何種的處理。

3.2.3. 總結(jié)

在本小節(jié)中我們對(duì)在使用 Mocha 進(jìn)行 Promise 測試時(shí)可能出現(xiàn)的一些意外情況進(jìn)行了介紹。

  • 普通的代碼采用 thencatch 的流程的話比較容易理解

    • 這是為了錯(cuò)誤處理的方便。請(qǐng)參考 then or catch?
  • 將測試代碼集中到 then 中處理
    • 為了能將 AssertionError 對(duì)象傳遞到測試框架中。

通過使用 .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 的測試代碼, 以及怎樣去編寫更容易理解的測試代碼。

3.3. 編寫可控測試(controllable tests)

在繼續(xù)進(jìn)行說明之前,我們先來定義一下什么是可控測試。在這里我們對(duì)可控測試的定義如下。

待測試的 promise 對(duì)象

  • 如果編寫預(yù)期為 Fulfilled 狀態(tài)的測試的話

    • Rejected 的時(shí)候要 Fail
    • assertion 的結(jié)果不一致的時(shí)候要 Fail
  • 如果預(yù)期為 Rejected 狀態(tài)的話
    • 結(jié)果為 Fulfilled 測試為 Fail
    • assertion 的結(jié)果不一致的時(shí)候要 Fail

如果一個(gè)測試能網(wǎng)羅上面的用例(Fail)項(xiàng),那么我們就稱其為可控測試。

也就是說,一個(gè)測試用例應(yīng)該包括下面的測試內(nèi)容。

  • 結(jié)果滿足 Fulfilled or Rejected 之一
  • 對(duì)傳遞給 assertion 的值進(jìn)行檢查

在前面使用了 .then 的代碼就是一個(gè)期望結(jié)果為 Rejected 的測試。


    promise.then(failTest, function(error){
        // 通過assert驗(yàn)證error對(duì)象
        assert(error instanceof Error);
    });

3.3.1. 必須明確指定轉(zhuǎn)換后的狀態(tài)

為了編寫有效的測試代碼, 我們需要明確指定 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 處理大同小異的代碼。

  1. 將需要測試的 promise 對(duì)象傳遞給 shouldRejected 方法
  2. 在返回的對(duì)象的 catch 方法中編寫進(jìn)行 onRejected 處理的代碼
  3. 在 onRejected 里使用 assertion 進(jìn)行判斷

在使用 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)換了。

3.3.2. 小結(jié)

在本小節(jié)我們學(xué)習(xí)了如何編寫針對(duì) Promise 特定狀態(tài)的測試代碼,以及如何使用便于測試的 helper 函數(shù)。

這里我們使用到的 shouldFulfilledshouldRejected 也可以在下面的類庫中找到。 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)格一致的測試則是非常重要。

上一篇:什么是 Promise下一篇:Advanced