你的第一個套件

套件:describe 你的測試

describe 函式用於分組相關規格,通常每個測試檔案在頂層都有其中一個。字串參數是用於命名規格集合,並將與規格合併以組成規格的全名。這有助於在較大的套件中找到規格。如果您為它們命名得當,您的規格會以傳統 BDD 樣式讀取為完整句子。

規格

規格是透過呼叫全域 Jasmine 函式 it 定義的,與 describe 相同,這會帶入一個字串和一個函式。這個字串就是規格的標題,函式就是規格或測試。規格包含一個或多個驗證,用於測試程式碼狀態。在 Jasmine 中,驗證是一種斷言,其為真或假。具有所有為真驗證的規格就是通過規格。具有至少一個為假驗證的規格就是失敗規格。

describe("A suite", function() {
    it("contains spec with an expectation", function() {
        expect(true).toBe(true);
    });
});

這只不過是函式

由於 describeit 區塊是函式,因此它們可以包含用於實作測試的任何可執行碼。JavaScript 範圍規則適用,因此在 describe 中宣告的變數可供套件內的任何 it 區塊使用。

describe("A suite is just a function", function() {
    let a;

    it("and so is a spec", function() {
        a = true;

        expect(a).toBe(true);
    });
});

驗證

驗證透過函式 expect 建置,它會帶入一個值,稱為實際值。它與比對函式串連,而比對函式會帶入預期值。

describe("The 'toBe' matcher compares with ===", function() {

比對函式

每個比對函式會在實際值和預期值之間實作一個布林比較。它負責向 Jasmine 報告驗證為真或假。Jasmine 之後會讓規格通過或失敗。

    it("and has a positive case", function() {
        expect(true).toBe(true);
    });

任何比對函式都可以透過在呼叫比對函式之前在 expect 呼叫上串連 not 來評估為否定驗證。

    it("and can have a negative case", function() {
        expect(false).not.toBe(true);
    });

Jasmine 附帶一組豐富的比對函式,您可以在 API 文件 中找到完整清單。對於專案網域要求 Jasmine 未包含的特定斷言時,也有撰寫 自訂比對函式 的功能。

});

設定和清除

為了協助測試套件移除重複設定和清除程式碼,Jasmine 提供全域 beforeEachafterEachbeforeAllafterAll 函式。

describe("A suite with some shared setup", function() {
    let foo = 0;

顧名思義,beforeEach 函式會在呼叫它的 describe 中的每個規格之前呼叫一次

    beforeEach(function() {
        foo += 1;
    });

afterEach 函式會在每個規格之後呼叫一次。

    afterEach(function() {
        foo = 0;
    });

beforeAll 函式僅在 describe 中的所有規格執行之前呼叫一次

    beforeAll(function() {
        foo = 1;
    });

afterAll 函式會在所有規格結束後呼叫。

    afterAll(function() {
        foo = 0;
    });
});

beforeAllafterAll 可用於加速設定和清除昂貴的測試套件。

但是,請小心使用 beforeAllafterAll!由於它們不會在規格之間重設,因此很容易意外讓狀態在規格之間洩漏,導致它們錯誤地通過或失敗。

this 關鍵字

透過 this 關鍵字,可以分享 beforeEachitafterEach 之間的變數。每個規格的 beforeEach/it/afterEach 都有 this,作為設定回空的同一空白物件,供下一個規格的 beforeEach/it/afterEach 使用。

注意:如果您要使用 this 關鍵字分享變數,您必須使用 function 關鍵字,而不是箭頭函式。

describe("A spec", function() {
    beforeEach(function() {
        this.foo = 0;
    });

    it("can use the `this` to share state", function() {
        expect(this.foo).toEqual(0);
        this.bar = "test pollution?";
    });

    it("prevents test pollution by having an empty `this` " +
            "created for the next spec", function() {
        expect(this.foo).toEqual(0);
        expect(this.bar).toBe(undefined);
    });
});

使用 fail 手動使規格失敗

fail 函式會造成規格失敗。它可以將失敗訊息或錯誤物件作為參數。

describe("A spec using the fail function", function() {
    function foo(x, callBack) {
        if (x) {
            callBack();
        }
    };

    it("should not call the callBack", function() {
        foo(false, function() {
            fail("Callback has been called");
        });
    });
});

嵌套 describe 區塊

describe 的呼叫可以嵌套,且規格定義在任何層級。這讓組件能組成函式的樹狀結構。在執行規格之前,Jasmine 會執行樹狀結構並依順序執行每個 beforeEach 函式。在執行規格之後,Jasmine 會在 afterEach 函式中以類似的形式執行。

describe("A spec", function() {
    let foo;

    beforeEach(function() {
        foo = 0;
        foo += 1;
    });

    afterEach(function() {
        foo = 0;
    });

    it("is just a function, so it can contain any code", function() {
        expect(foo).toEqual(1);
    });
    
    it("can have more than one expectation", function() {
        expect(foo).toEqual(1);
        expect(true).toEqual(true);
    });

    describe("nested inside a second describe", function() {
        let bar;

        beforeEach(function() {
          bar = 1;
        });
    
        it("can reference both scopes as needed", function() {
          expect(foo).toEqual(bar);
        });
    });
});

停用組件

組件可以使用 xdescribe 函式停用。這些組件和它們之中的任何規格執行時都會略過,因此其結果會顯示為保留中。

組件也可以使用 fdescribe 函式進行重點關注。這表示只有 fdescribe 組件會執行。

xdescribe("A spec", function() {
    let foo;
    
    beforeEach(function() {
        foo = 0;
        foo += 1;
    });
    
    it("is just a function, so it can contain any code", function() {
        expect(foo).toEqual(1);
    });
});

保留中規格

保留中規格不會執行,但其名稱會在結果中顯示為 pending

describe("Pending specs", function() {

使用 xit 宣告的任何規格都會標記為保留中。

    xit("can be declared 'xit'", function() {
        expect(true).toBe(false);
    });

沒有函式本體的任何規格在結果中也會標記為保留中。

    it("can be declared with 'it' but without a function");

如果你在規格本體的任意處呼叫 pending 函式,不論預期結果為何,規格將被標記為保留中。傳入 pending 字串將被視為原因,並在組件完成時顯示。

測試也可以使用 fit 函式進行重點關注。這表示只有 fit 測試會執行。

    it("can be declared by calling 'pending' in the spec body", function() {
        expect(true).toBe(false);
        pending('this is why it is pending');
    });
});

偵測器

Jasmine 具有稱為偵測器的測試替身函式。偵測器可以偽造任何函式,並追蹤其呼叫及其所有引數。偵測器僅存在於定義它的 describeit 區塊中,且在每個規格後都會移除。有針對互動偵測器的特殊比對函式。

describe("A spy", function() {
    let foo;
    let bar = null;

    beforeEach(function() {
        foo = {
            setBar: function(value) {
                bar = value;
            }
        };

您可以使用 and 定義偵測器在呼叫時將執行什麼動作。

        spyOn(foo, 'setBar');

        foo.setBar(123);
        foo.setBar(456, 'another param');
    });

如果偵測器被呼叫,toHaveBeenCalled 比對函式會通過。

    it("tracks that the spy was called", function() {
        expect(foo.setBar).toHaveBeenCalled();
    });

如果偵測器被呼叫指定次數,toHaveBeenCalledTimes 比對函式會通過。

    it("tracks that the spy was called x times", function() {
        expect(foo.setBar).toHaveBeenCalledTimes(2);
    });

如果引數清單與偵測器的任何已記錄呼叫相符,toHaveBeenCalledWith 比對函式將回傳 true。

    it("tracks all the arguments of its calls", function() {
        expect(foo.setBar).toHaveBeenCalledWith(123);
        expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');
    });

    it("stops all execution on a function", function() {
        expect(bar).toBeNull();
    });

您可以使用 calls 取得偵測器追蹤其呼叫的所有資料。

    it("tracks if it was called at all", function() {
        foo.setBar();

        expect(foo.setBar.calls.any()).toEqual(true);
    });
});

偵測器:createSpy

如果沒有函式要偵測,jasmine.createSpy 可以建立「空白」偵測器。此偵測器與其他偵測器一樣,會追蹤呼叫、引數等等。但是它背後沒有實作。

describe("A spy, when created manually", function() {
    let whatAmI;

    beforeEach(function() {
        whatAmI = jasmine.createSpy('whatAmI');

        whatAmI("I", "am", "a", "spy");
    });

    it("tracks that the spy was called", function() {
        expect(whatAmI).toHaveBeenCalled();
    });
});

偵測器:createSpyObj

若要建立具有多重偵測器之一組,請使用 jasmine.createSpyObj 並傳入一組字串。它會回傳一個物件,其中每一個字串的屬性都是一個偵測器。

describe("Multiple spies, when created manually", function() {
    let tape;

    beforeEach(function() {
        tape = jasmine.createSpyObj(
            'tape',
            ['play', 'pause', 'stop', 'rewind']
        );

        tape.play();
        tape.pause();
        tape.rewind(0);
    });

    it("creates spies for each requested function", function() {
        expect(tape.play).toBeDefined();
        expect(tape.pause).toBeDefined();
        expect(tape.stop).toBeDefined();
        expect(tape.rewind).toBeDefined();
    });
});

更精緻的比對

有時您不想使用完全相等來進行比對。Jasmine 提供多項非對稱的相等測試工具。

describe("Matching with finesse", function() {

jasmine.any 將建構函式或「類別」名稱視為預期值。如果建構函式與實際值的建構函式相符,它會回傳 true

    describe("jasmine.any", function() {
        it("matches any value", function() {
            expect({}).toEqual(jasmine.any(Object));
            expect(12).toEqual(jasmine.any(Number));
        });
    
        describe("when used with a spy", function() {
            it("is useful for comparing arguments", function() {
                const foo = jasmine.createSpy('foo');
                foo(12, function() {
                    return true;
                });
    
                expect(foo).toHaveBeenCalledWith(
                    jasmine.any(Number), jasmine.any(Function)
                );
            });
        });
    });

jasmine.anything 如果實際值不為 nullundefined,則會回傳 true

    describe("jasmine.anything", function() {
        it("matches anything", function() {
            expect(1).toEqual(jasmine.anything());
        });

        describe("when used with a spy", function() {
            it("is useful when the argument can be ignored", function() {
                const foo = jasmine.createSpy('foo');
                foo(12, function() {
                    return false;
                });

                expect(foo).toHaveBeenCalledWith(12, jasmine.anything());
            });
        });
    });

jasmine.objectContaining 適用於預期值僅關心實際值中特定鍵值對的情況。

    describe("jasmine.objectContaining", function() {
        let foo;

        beforeEach(function() {
            foo = {
                a: 1,
                b: 2,
                bar: "baz"
            };
        });

        it("matches objects with the expect key/value pairs", function() {
            expect(foo).toEqual(jasmine.objectContaining({
                bar: "baz"
            }));
            expect(foo).not.toEqual(jasmine.objectContaining({
                c: 37
            }));
        });

        describe("when used with a spy", function() {
            it("is useful for comparing arguments", function() {
                const callback = jasmine.createSpy('callback');

                callback({
                    bar: "baz"
                });

                expect(callback).toHaveBeenCalledWith(
                    jasmine.objectContaining({ bar: "baz" })
                );
            });
        });
    });

jasmine.arrayContaining 適用於預期值僅關心陣列中一些數值的情況。

    describe("jasmine.arrayContaining", function() {
        let foo;

        beforeEach(function() {
            foo = [1, 2, 3, 4];
        });

        it("matches arrays with some of the values", function() {
            expect(foo).toEqual(jasmine.arrayContaining([3, 1]));
            expect(foo).not.toEqual(jasmine.arrayContaining([6]));
        });

        describe("when used with a spy", function() {
            it("is useful when comparing arguments", function() {
                const callback = jasmine.createSpy('callback');

                callback([1, 2, 3, 4]);

                expect(callback).toHaveBeenCalledWith(
                    jasmine.arrayContaining([4, 2, 3])
                );
                expect(callback).not.toHaveBeenCalledWith(
                    jasmine.arrayContaining([5, 2])
                );
            });
        });
    });

jasmine.stringMatching 適用於您不想在較大物件中完全比對字串,或在偵測器預期值中比對字串中的一部分之情況。

    describe('jasmine.stringMatching', function() {
        it("matches as a regexp", function() {
            expect({foo: 'bar'}).toEqual({
                foo: jasmine.stringMatching(/^bar$/)
            });
            expect({foo: 'foobarbaz'}).toEqual({
                foo: jasmine.stringMatching('bar')
            });
        });

        describe("when used with a spy", function() {
            it("is useful for comparing arguments", function() {
                const callback = jasmine.createSpy('callback');

                callback('foobarbaz');

                expect(callback).toHaveBeenCalledWith(
                    jasmine.stringMatching('bar')
                );
                expect(callback).not.toHaveBeenCalledWith(
                    jasmine.stringMatching(/^bar$/)
                );
            });
        });
    });

自訂非對稱相等測試工具

當您需要檢查某事物是否符合特定條件,但並非嚴格相等時,也可以指定自訂非對稱相等測試工具,方法是提供具有 asymmetricMatch 函式的物件。

    describe("custom asymmetry", function() {
        const tester = {
            asymmetricMatch: function(actual) {
                const secondValue = actual.split(',')[1];
                return secondValue === 'bar';
            }
        };

        it("dives in deep", function() {
            expect("foo,bar,baz,quux").toEqual(tester);
        });

        describe("when used with a spy", function() {
            it("is useful for comparing arguments", function() {
                const callback = jasmine.createSpy('callback');

                callback('foo,bar,baz');

                expect(callback).toHaveBeenCalledWith(tester);
            });
        });
    });
});

Jasmine 時間

可以使用 Jasmine 時間 來測試依賴時間的程式碼。

describe("Manually ticking the Jasmine Clock", function() {
    let timerCallback;

會在需要操控時間的 spec 或 suite 中呼叫 jasmine.clock().install 來安裝它。

    beforeEach(function() {
        timerCallback = jasmine.createSpy("timerCallback");
        jasmine.clock().install();
    });

用完時務必解除安裝時鐘,以回復原來的功能。

    afterEach(function() {
        jasmine.clock().uninstall();
    });

模擬 JavaScript Timeout 函數

你可以讓 setTimeoutsetInterval 同步執行已註冊函數,只要時鐘時間前進即可。

要執行已註冊函數,請透過 jasmine.clock().tick 函數前進時間,這個函數會用毫秒為單位。

    it("causes a timeout to be called synchronously", function() {
        setTimeout(function() {
            timerCallback();
        }, 100);

        expect(timerCallback).not.toHaveBeenCalled();

        jasmine.clock().tick(101);

        expect(timerCallback).toHaveBeenCalled();
    });

    it("causes an interval to be called synchronously", function() {
        setInterval(function() {
            timerCallback();
        }, 100);

        expect(timerCallback).not.toHaveBeenCalled();

        jasmine.clock().tick(101);
        expect(timerCallback.calls.count()).toEqual(1);

        jasmine.clock().tick(50);
        expect(timerCallback.calls.count()).toEqual(1);

        jasmine.clock().tick(50);
        expect(timerCallback.calls.count()).toEqual(2);
    });

模擬日期

Jasmine 時鐘也可以用來模擬現在的日期。

    describe("Mocking the Date object", function(){
        it("mocks the Date object and sets it to a given time", function() {
            const baseTime = new Date(2013, 9, 23);

如果沒有提供 mockDate 的基本時間,它就會使用現在的日期。

            jasmine.clock().mockDate(baseTime);

            jasmine.clock().tick(50);
            expect(new Date().getTime()).toEqual(baseTime.getTime() + 50);
        });
    });
});

非同步支援

Jasmine 也支援執行需要測試非同步作業的規範。傳遞給 beforeAllafterAllbeforeEachafterEach 以及 it 的函數可以宣告為 async

Jasmine 也支援明確回傳承諾或需要回呼的非同步函數。更多資訊請參閱 非同步作業教學

describe("Using async/await", function() {
    beforeEach(async function() {
        await soon();
        value = 0;
    });

這個規範直到上面呼叫 beforeEach 回傳的承諾完成後才會開始。而且這個規範直到它回傳的承諾完成後才會完成。

    it("supports async execution of test preparation and expectations",
        async function() {
            await soon();
            value++;
            expect(value).toBeGreaterThan(0);
        }
    );
});

Jasmine 預設會針對非同步規範等候 5 秒,才會使它逾時失敗。如果 done 在逾時時間到之前呼叫,目前的規範會被標示為失敗,而且 suite 執行會繼續,就像已呼叫 done 一樣。

如果具體規範應該更快速失敗或需要更多時間,可以用傳遞逾時值給 it 等的方式來調整。

如果整個 suite 應該有不同的逾時時間,可以設定 jasmine.DEFAULT_TIMEOUT_INTERVAL 為全域變數,出現在任何給定的 describe 之外。

describe("long asynchronous specs", function() {
    beforeEach(async function() {
        await somethingSlow();
    }, 1000);

    it("takes a long time", function() {
        await somethingReallySlow();
    }, 10000);

    afterEach(function() {
        await somethingSlow();
        }, 1000);
    });

    function soon() {
        return new Promise(function(resolve, reject) {
            setTimeout(function() {
                resolve();
            }, 1);
        });
    }
});