Jasmine 使用教程

前言

隨著前端開發的復雜度與日俱增,前端工程師寫出來的JS代碼變得越來越龐大,從而使項目變的臃腫,維護的成本及難度不斷加大,協作迭代開發已經很難保證代碼的功能性不被破壞。所以在這種情況下,考慮引入一款前端的單元測試框架,來為代碼保駕護航是一件很重要的事情。

什么是單元測試

或許大家在看一些產品書籍的時候,當介紹一個項目時,會不由自主說到測試。 比如,單元測試,函數測試,或是TDD,BDD等測試模式。

那究竟什么是單元測試呢?
單元測試,單元,測試。

什么是單元(unit)?單元就是相對獨立的功能模塊。一個完整的、模塊化的程序,都是由許多單元構成,單元完成自己的任務、然后與其它單元進行交互,最終協同完成整個程序功能。

什么是測試?測試就是測試,判斷測試對象對于某個特定的輸入有沒有預期的輸出。

所以什么是單元測試?就是對構成程序的每個單元進行測試(大霧)。工程上的一個共識是,如果程序的每個模塊都是正確的、模塊與模塊的連接是正確的、那么程序基本上就會是正確的。

所以單元測試就是這么一個概念,一種努力保證構成程序的每個模塊的正確性,從而保證整個程序的正確性的方法論。

說白了,單元測試的目的就是來保證你寫的JS模塊能夠完成任務并且沒有出現bug,如果你的代碼在單元測試的過程中,需要引入多個模塊時,這說明你測試的主體模塊的耦合度相對比較高,也就意味著你要重構代碼了。

單元測試的模式

單元測試的模式基本分為:TDD 和 BDD。

TDD 全稱是 Test-driven development,即測試驅動開發。

TDD 的原理是在開發功能代碼之前,先編寫單元測試用例代碼,測試代碼確定需要編寫什么產品代碼。

TDD 的基本思路就是通過測試來推動整個開發的進行,但測試驅動開發并不只是單純的測試工作,而是把需求分析,設計,質量控制量化的過程。

TDD 首先考慮使用需求(對象、功能、過程、接口等),主要是編寫測試用例框架對功能的過程和接口進行設計,而測試框架可以持續進行驗證。

通常的TDD測試步驟是:

先寫測試
再寫代碼
測試
重構
通過
BDD 全稱是: Behavior-Driven development,即行為驅動開發。

BDD 的應用場景就是給一些 QA 工程師使用的,他用他的語言和你進行交流,即他會進行一些測試用例,然后如果通過則說明,他已經信賴你了。

而 BDD 與 TDD 的主要區別是,使得非程序人員也能參與到測試用例的編寫中來,大大降低了客戶、用戶、項目管理者與開發者之間來回翻譯的成本。所以BDD更加注重業務需求而不是技術,而目前在大部分公司里面,通常使用的是BDD測試。而我們本文的重點就是講一款BDD模式的測試框架,那就是 Jasmine。

什么是Jasmine

首先,Jasmine是一款JavaScript 測試框架,它不依賴于其他任何 JavaScript 組件。它有干凈清晰的語法,讓您可以很簡單的寫出測試代碼。

其次,Jasmine官網介紹里面開篇第一句話是“Jasmine is a behavior-driven development framework for testing JavaScript code.”,主要的意思就是說它是一款BDD模式的測試框架,也就是行為驅動開發,同樣它是一種敏捷軟件開發的技術。

BDD 更像是一種團隊的約定,javascript 單元測試,也許對于你本人(開發該腳本的前端)意義不是特別突出,但對于整個團隊,整個項目來說就是一種財富

如何使用Jasmine

官網文檔地址:http://jasmine.github.io/2.3/introduction.html

1、在項目根目錄中,初始化 package.json

npm init

2、目錄結構:

- src
    - index.js
- test
    - indexTest.js
package.json

3、安裝 karma + jasmine 相關包

npm install -g karma-cli
npm install karma karma-jasmine karma-chrome-launcher jasmine-core --save-dev

4、開啟 Karma

karma start
image

手動打開Chrome,輸入localhost:9876

image

5、初始化 karma

karma init
image

說明:

  1. 測試框架:我們當然選jasmine

  2. 是否添加 Require.js 插件

  3. 選擇瀏覽器: 我們選Chrome

  4. 測試文件路徑設置,文件可以使用通配符匹配,比如*.js匹配指定目錄下所有的js文件(實際操作中發現該路徑是 karma.conf.js 文件的相對路徑,詳見下面我給出的實際測試配置及說明)

  5. 在測試文件路徑下,需要排除的文件

  6. 是否允許 Karma 監測文件,yes 表示當測試路徑下的文件變化時,Karma 會自動測試

以下是 karma.conf.js 的完整內容:
// Karma configuration
// Generated on Wed Nov 16 2016 14:26:14 GMT+0800 (中國標準時間)

module.exports = function(config) {
    config.set({

        // base path that will be used to resolve all patterns (eg. files, exclude)
        basePath: '',

        // frameworks to use
        // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
        frameworks: ['jasmine'],

        // list of files / patterns to load in the browser
        files: [
            'src/**/*.js',
            'test/**/*.js'
        ],

        // list of files to exclude
        exclude: [],

        // preprocess matching files before serving them to the browser
        // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
        preprocessors: {
           
        },

        // test results reporter to use
        // possible values: 'dots', 'progress'
        // available reporters: https://npmjs.org/browse/keyword/karma-reporter
        reporters: ['progress'],

        // web server port
        port: 9876,

        // enable / disable colors in the output (reporters and logs)
        colors: true,

        // level of logging
        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
        logLevel: config.LOG_INFO,

        // enable / disable watching file and executing tests whenever any file changes
        autoWatch: true,

        // start these browsers
        // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
        browsers: ['Chrome'],

        // Continuous Integration mode
        // if true, Karma captures browsers, runs the tests and exits
        singleRun: false,

        // Concurrency level
        // how many browser should be started simultaneous
        concurrency: Infinity
    })
}

index.js 內容:

function reverse(name){
    if(name === 'AAA') return 'BBB';
    return name.split("").reverse().join("");
}

indexTest.js 內容:

describe("全部變量,定義測試", function() {

    beforeEach(function(){

    });

    afterEach(function(){
        
    });

    it("reverse word", function(){
        expect("DCBA").toBe(reverse("ABCD"));
    });
});

啟動 karma:

karma start

因為我們在配置里設置了在 Chrome 中測試,因此 karma 會自動啟動 Chrome 實例,并運行測試用例:

image

如果我們點擊圖中的 debug 按鈕,進入 debug.html 并按 F12 打開開發者工具,選擇 Console 窗口,我們將能看到 jasmine 的執行日志

image

這個時候,說明我們已經部署成功,并且可以進行測試用例編寫。

代碼覆蓋率

如果你還想查看測試的代碼覆蓋率,我們可以安裝karma-coverage插件,安裝命令為:

npm install karma-coverage --save-dev

修改 karma.conf.js,增加覆蓋率的配置:

preprocessors: {
    'src/**/*.js': ['coverage']
}

reporters: ['progress', 'coverage']

// add
coverageReporter: {
    type: 'html',
    dir: 'coverage/'
}

變動如下:

  • 在 reporters 中增加 coverage
  • preprocessors 中指定 js 文件
  • 添加 coverageReporter 節點,將覆蓋率報告類型 type 設置為 html,輸入目錄 dir 指定到你希望的目錄中

啟動 Karma

karma start

(執行命令后,在配置文件 coverageReporter 節點中指定的 dir 中,我們將找到生成的覆蓋率報告,karma-coverage 還生成了一層子文件夾,對應于執行測試的瀏覽器+版本號+操作系統版本)
image

使用 jasmine-html

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Jasmine Spec Runner v2.4.1</title>
    <link rel="shortcut icon" type="image/png" href="../jasmine-2.4.1/images/jasmine_favicon.png">
    <link rel="stylesheet" type="text/css" href="../jasmine-2.4.1/lib/jasmine-core/jasmine.css">

    <script type="text/javascript" src="../jasmine-2.4.1/lib/jasmine-core/jasmine.js"></script>
    <script type="text/javascript" src="../jasmine-2.4.1/lib/jasmine-core/jasmine-html.js"></script>
    <script type="text/javascript" src="../jasmine-2.4.1/lib/jasmine-core/boot.js"></script>

    <!-- 需要測試的js文件及jasmine測試腳本 -->
    <script type="text/javascript" src="myFirstJasmineTest.js"></script>
</head>
<body>

</body>
</html>
myFirstJasmineTest
/*
Created by laixiangran on 2015/12/15.
jasmine測試腳本
 */
(function() {
    /*
     jasmine基本語法介紹:
     describe(string, function):可以理解為是一個測試集或者測試包(官方稱之為suite),主要功能是用來劃分單元測試的,describe是可以嵌套使用的
     參數string:描述測試包的信息
     參數function:測試集的具體實現,可包含任意代碼

     it(string, function):測試用例(官方稱之為spec)
     參數string:描述測試用例的信息
     參數function:測試用例的具體實現,可包含任意代碼

     expect:斷言表達式

     從以下例子可知:
     1、每個測試文件中可以包含多個describe
     2、每個describe中可以包含多個it
     3、每個it中可以包含多個expect
     4、describe可嵌套使用
     */
    describe("Jasmine Test 1", function() {
        it("a spec with an expectation", function() {
            expect(1).toBe(1);
            expect(1===1).toBe(true);
            expect("a").not.toBe("b");
        });

        it("an other spec in current suite", function() {
            expect(true).toBe(true);
        });
    });
    describe("Jasmine Test 2", function() {
        it("nothing", function() {

        });
    });
    describe("Jasmine Test 3", function() {
        describe("Jasmine Test 4", function() {
            it("b等于b", function() {
                expect("b").toBe("b");
            });

            it("1===1是正確的", function() {
                expect(1===1).toBe(true);
            });
        });
    });

    /*
     * expect的匹配器
     * */
    describe("Included matchers:", function() {
        //"toBe"基本類型判斷
        it("The 'toBe' matcher compares with ===", function() {
            var a = 12;
            var b = a;
            expect(a).toBe(b);
            expect(a).not.toBe(null);
        });
        //"toEqual"除了能判斷基本類型(相當于"toBe"),還能判斷對象
        describe("The 'toEqual' matcher", function() {
            //基本類型判斷
            it("works for simple literals and variables", function() {
                var a = 12;
                expect(a).toEqual(12);
            });
            //對象判斷
            it("should work for objects", function() {
                var foo = {
                    a: 12,
                    b: 34
                };
                var bar = {
                    a: 12,
                    b: 34
                };
                expect(foo).toEqual(bar);
            });
        });
        //"toMatch"使用正則表達式判斷
        it("The 'toMatch' matcher is for regular expressions", function() {
            var message = "foo bar baz";
            expect(message).toMatch(/bar/);
            expect(message).toMatch("bar");
            expect(message).not.toMatch(/quux/);
        });
        //"toBeDefined"判斷是否定義
        it("The 'toBeDefined' matcher compares against 'undefined'", function() {
            var a = {
                foo: "foo"
            };
            expect(a.foo).toBeDefined();
            expect(a.bar).not.toBeDefined();
        });
        //"toBeUndefined"判斷是否是undefined,與"toBeDefined"相反
        it("The 'toBeUndefined' matcher compares against 'undefined'", function() {
            var a = {
                foo: "foo"
            };
            expect(a.foo).not.toBeUndefined();
            expect(a.bar).toBeUndefined();
        });
        //"toBeNull"判斷是否為null
        it("The 'toBeNull' matcher compares against null", function() {
            var a = null;
            var foo = "foo";
            expect(null).toBeNull();
            expect(a).toBeNull();
            expect(foo).not.toBeNull();
        });
        //"toBeTruthy"判斷是否是true
        it("The 'toBeTruthy' matcher is for boolean casting testing", function() {
            var a, foo = "foo";
            expect(foo).toBeTruthy();
            expect(a).not.toBeTruthy();
            expect(true).toBeTruthy();
        });
        //"toBeFalsy"判斷是否是false
        it("The 'toBeFalsy' matcher is for boolean casting testing", function() {
            var a, foo = "foo";
            expect(a).toBeFalsy();
            expect(foo).not.toBeFalsy();
            expect(false).toBeFalsy();
        });
        //"toContain"判斷數組是否包含(可判斷基本類型和對象)
        it("The 'toContain' matcher is for finding an item in an Array", function() {
            var a = ["foo", "bar", "baz"];
            var b = [{foo: "foo", bar: "bar"}, {baz: "baz", bar: "bar"}];
            expect(a).toContain("bar");
            expect(a).not.toContain("quux");
            expect(b).toContain({foo: "foo", bar: "bar"});
            expect(b).not.toContain({foo: "foo", baz: "baz"});
        });
        //"toBeLessThan"判斷值類型的大小,結果若小則為True(也可以判斷字符及字符串,以ascii碼的大小為判斷依據)
        it("The 'toBeLessThan' matcher is for mathematical comparisons", function() {
            var pi = 3.1415926,
                e = 2.78;
            expect(e).toBeLessThan(pi);
            expect(pi).not.toBeLessThan(e);
            expect("a").toBeLessThan("b");
            expect("b").not.toBeLessThan("a");
        });
        //"toBeGreaterThan"判斷值類型的大小,結果若大則為True,與toBeLessThan相反(也可以判斷字符及字符串,以ascii碼的大小為判斷依據)
        it("The 'toBeGreaterThan' matcher is for mathematical comparisons", function() {
            var pi = 3.1415926,
                e = 2.78;
            expect(pi).toBeGreaterThan(e);
            expect(e).not.toBeGreaterThan(pi);
            expect("a").not.toBeGreaterThan("b");
            expect("b").toBeGreaterThan("a");
        });
        //"toBeCloseTo"判斷數字是否相似(第二個參數為小數精度,默認為2位)
        it("The 'toBeCloseTo' matcher is for precision math comparison", function() {
            var a = 1.1;
            var b = 1.5;
            var c = 1.455;
            var d = 1.459;
            expect(a).toBeCloseTo(b, 0);
            expect(a).not.toBeCloseTo(c, 1);
            expect(c).toBeCloseTo(d);
        });
        //"toThrow"判斷是否拋出異常
        it("The 'toThrow' matcher is for testing if a function throws an exception", function() {
            var foo = function() {
                return 1 + 2;
            };
            var bar = function() {
                return a + 1;
            };
            expect(foo).not.toThrow();
            expect(bar).toThrow();
        });
        //"toThrowError"判斷是否拋出了指定的錯誤
        it("The 'toThrowError' matcher is for testing a specific thrown exception", function() {
            var foo = function() {
                throw new TypeError("foo bar baz");
            };
            expect(foo).toThrowError("foo bar baz");
            expect(foo).toThrowError(/bar/);
            expect(foo).toThrowError(TypeError);
            expect(foo).toThrowError(TypeError, "foo bar baz");
        });
    });

    /*
     * "fail"函數能使一個測試用例失敗,參數為自定義的失敗信息
     * */
    describe("A spec using the fail function", function() {
        var foo = function(x, callBack) {
            if (x) {
                callBack();
            }
        };

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

    /*
     Jasmine允許在執行測試集/測試用例的開始前/結束后做一些初始化/銷毀的操作。

     Setup方法:
     beforeAll:每個suite(即describe)中所有spec(即it)運行之前運行
     beforeEach:每個spec(即it)運行之前運行

     Teardown方法:
     afterAll:每個suite(即describe)中所有spec(即it)運行之后運行
     afterEach:每個spec(即it)運行之后運行
     * */
    var globalCount;
    describe("Setup and Teardown suite 1", function() {
        var suiteGlobalCount;
        var eachTestCount;

        beforeAll(function() {
            globalCount = 0;
            suiteGlobalCount = 0;
            eachTestCount = 0;
        });

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

        beforeEach(function() {
            globalCount++;
            suiteGlobalCount++;
            eachTestCount++;
        });

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

        it("Spec 1", function() {
            expect(globalCount).toBe(1);
            expect(suiteGlobalCount).toBe(1);
            expect(eachTestCount).toBe(1);
        });

        it("Spec 2", function() {
            expect(globalCount).toBe(2);
            expect(suiteGlobalCount).toBe(2);
            expect(eachTestCount).toBe(1);
        });
    });

    describe("Setup and Teardown suite 2", function() {
        beforeEach(function() {
            globalCount += 2;
        });

        it("Spec 1", function() {
            expect(globalCount).toBe(4);
        });
    });

    /*
     * 在beforeEach - it - afterEach中,還可以使用this關鍵字定義變量。需要注意的是,使用this關鍵字聲明的變量,僅在beforeEach - it - afterEach這個過程中傳遞
     * */
    describe("Test 'this'", function() {
        beforeEach(function() {
            this.testCount = this.testCount || 0;
            this.testCount++;
        });

        afterEach(function() {
            //this.testCount = 0; //無論是否有這行,結果是一樣的,因為this指定的變量只能在每個spec的beforeEach/it/afterEach過程中傳遞
        });

        it("Spec 1", function() {
            expect(this.testCount).toBe(1);
        });

        it("Spec 2", function() {
            expect(this.testCount).toBe(1);
        });
    });

    /*
    在實際項目中,需要由于發布的版本需要選擇測試用例包,xdescribe和xit能很方便的將不包含在版本中的測試用例排除在外。
    不過xdescribe和xit略有不同:
    xdescribe:該describe下的所有it將被忽略,Jasmine將直接忽略這些it,因此不會被運行
    xit:運行到該it時,掛起它不執行
    * */
    xdescribe("Test xdescribe", function() {
        it("Spec 1", function() {
            expect(1).toBe(1);
        });

        it("Spec 2", function() {
            expect(2).toBe(2);
        });
    });

    describe("Test xit", function() {
        it("Spec 1", function() {
            expect(1).toBe(1);
        });

        xit("Spec 2", function() {
            expect(2).toBe(1);
        });

        xit("Spec 3", function() {
            expect(3).toBe(3);
        });
    });

    /*
    * Spy用來追蹤函數的調用歷史信息(是否被調用、調用參數列表、被請求次數等)。Spy僅存在于定義它的describe和it方法塊中,并且每次在spec執行完之后被銷毀。
    * 當在一個對象上使用spyOn方法后即可模擬調用對象上的函數,此時對所有函數的調用是不會執行實際代碼的。
    * 兩個Spy常用的expect:
    *   toHaveBeenCalled: 函數是否被調用
    *   toHaveBeenCalledWith: 調用函數時的參數
    * */
    describe("A spy", function() {
        var foo, bar = null;

        beforeEach(function() {
            foo = {
                setBar: function(value) {
                    bar = value;
                }
            };
            spyOn(foo, "setBar"); // 在foo對象上添加spy
            // 此時調用foo對象上的方法,均為模擬調用,因此不會執行實際的代碼
            foo.setBar(123); // 調用foo的setBar方法
            foo.setBar(456, "another param");
        });

        it("tracks that the spy was called", function() {
            expect(foo.setBar).toHaveBeenCalled(); //判斷foo的setBar是否被調用
        });

        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();  // 由于是模擬調用,因此bar值并沒有改變
        });
    });

    /*
    * spyOn().and.callThrough(),告訴Jasmine我們除了要完成對函數調用的跟蹤,同時也需要執行實際的代碼。
    * */
    describe("A spy, when configured to call through", function() {
        var foo, bar, fetchedBar;

        beforeEach(function() {
            foo = {
                setBar: function(value) {
                    bar = value;
                },
                getBar: function() {
                    return bar;
                }
            };
            spyOn(foo, "getBar").and.callThrough(); // 這里使用了callThrough,這時所有的函數調用為真實的執行
            spyOn(foo, "setBar").and.callThrough();
            foo.setBar(123);
            fetchedBar = foo.getBar();
        });

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

        it("should not effect other functions", function() {
            expect(foo.setBar).toHaveBeenCalledWith(123);
            expect(bar).toEqual(123); // 由于是真實調用,因此bar有了真實的值
        });

        it("when called returns the requested value", function() {
            expect(fetchedBar).toEqual(123); // 由于是真實調用,fetchedBar也有了真實的值
        });
    });

    /*
    * spyOn().and.returnValue(),由于Spy是模擬函數的調用,因此我們也可以強制指定函數的返回值。
    * */
    describe("A spy, when configured to fake a return value", function() {
        var foo, bar, fetchedBar;

        beforeEach(function() {
            foo = {
                setBar: function(value) {
                    bar = value;
                },
                getBar: function() {
                    return bar;
                }
            };
            spyOn(foo, "getBar").and.returnValue(745); // 這將指定getBar方法返回值為745
            foo.setBar(123);
            fetchedBar = foo.getBar();
        });

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

        it("should not effect other functions", function() {
            expect(bar).toEqual(123);
        });

        it("when called returns the requested value", function() {
            expect(fetchedBar).toEqual(745);
        });
    });

    /*
     * spyOn().and.callFake(),
     * 與returnValue相似,callFake則更進一步,直接通過指定一個假的自定義函數來執行。這種方式比returnValue更靈活,我們可以任意捏造一個函數來達到我們的測試要求。
     * */
    describe("A spy, when configured with an alternate implementation", function() {
        var foo, bar, fetchedBar;

        beforeEach(function() {
            foo = {
                setBar: function(value) {
                    bar = value;
                },
                getBar: function() {
                    return bar;
                }
            };
            spyOn(foo, "getBar").and.callFake(function() {
                return 1001;
            });
            foo.setBar(123);
            fetchedBar = foo.getBar();
        });

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

        it("should not effect other functions", function() {
            expect(bar).toEqual(123);
        });

        it("when called returns the requested value", function() {
            expect(fetchedBar).toEqual(1001);
        });
    });

    /*
     * spyOn().and.throwError(),模擬異常的拋出
     * */
    describe("A spy, when configured to throw an error", function() {
        var foo, bar;

        beforeEach(function() {
            foo = {
                setBar: function(value) {
                    bar = value;
                }
            };
            spyOn(foo, "setBar").and.throwError("quux");
        });

        it("throws the value", function() {
            expect(function() {
                foo.setBar(123)
            }).toThrowError("quux");
        });
    });

    /*
     * spyOn().and.stub(),回復到原始的spyOn()方法
     * */
    describe("A spy", function() {
        var foo, bar = null;

        beforeEach(function() {
            foo = {
                setBar: function(value) {
                    bar = value;
                },
                getBar: function(){
                    return bar;
                }
            };
            spyOn(foo, "setBar").and.callThrough(); // 標記1
            spyOn(foo, "getBar").and.returnValue(999); // 標記2
        });

        it("can call through and then stub in the same spec", function() {
            foo.setBar(123);
            expect(bar).toEqual(123);

            var getValue = foo.getBar();
            expect(getValue).toEqual(999);

            foo.setBar.and.stub(); // 相當于"標記1"中的代碼變為了spyOn(foo, "setBar")
            foo.getBar.and.stub(); // 相當于"標記2"中的代碼變為了spyOn(foo, "getBar")
            bar = null;

            foo.setBar(123);
            expect(bar).toBe(null);
            expect(foo.setBar).toHaveBeenCalled(); // 函數調用追蹤并沒有被重置

            getValue = foo.getBar();
            expect(getValue).toEqual(undefined);
            expect(foo.getBar).toHaveBeenCalled(); // 函數調用追蹤并沒有被重置
        });
    });

    /*
    * 其他追蹤屬性:
     calls:對于被Spy的函數的調用,都可以在calls屬性中跟蹤。
     .calls.any(): 被Spy的函數一旦被調用過,則返回true,否則為false;
     .calls.count(): 返回被Spy的函數的被調用次數;
     .calls.argsFor(index): 返回被Spy的函數的調用參數,以index來指定參數;
     .calls.allArgs():返回被Spy的函數的所有調用參數;
     .calls.all(): 返回calls的上下文,這將返回當前calls的整個實例數據;
     .calls.mostRecent(): 返回calls中追蹤的最近一次的請求數據;
     .calls.first(): 返回calls中追蹤的第一次請求的數據;
     .object: 當調用all(),mostRecent(),first()方法時,返回對象的object屬性返回的是當前上下文對象;
     .calls.reset(): 重置Spy的所有追蹤數據;
    * */
    describe("A spy", function() {
        var foo, bar = null;

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

        //.calls.any(): 被Spy的函數一旦被調用過,則返回true,否則為false;
        it("tracks if it was called at all", function() {
            expect(foo.setBar.calls.any()).toEqual(false);
            foo.setBar();
            expect(foo.setBar.calls.any()).toEqual(true);
        });

        //.calls.count(): 返回被Spy的函數的被調用次數;
        it("tracks the number of times it was called", function() {
            expect(foo.setBar.calls.count()).toEqual(0);
            foo.setBar();
            foo.setBar();
            expect(foo.setBar.calls.count()).toEqual(2);
        });

        //.calls.argsFor(index): 返回被Spy的函數的調用參數,以index來指定參數;
        it("tracks the arguments of each call", function() {
            foo.setBar(123);
            foo.setBar(456, "baz");
            expect(foo.setBar.calls.argsFor(0)).toEqual([123]);
            expect(foo.setBar.calls.argsFor(1)).toEqual([456, "baz"]);
        });

        //.calls.allArgs():返回被Spy的函數的所有調用參數;
        it("tracks the arguments of all calls", function() {
            foo.setBar(123);
            foo.setBar(456, "baz");
            expect(foo.setBar.calls.allArgs()).toEqual([[123],[456, "baz"]]);
        });

        //.calls.all(): 返回calls的上下文,這將返回當前calls的整個實例數據;
        it("can provide the context and arguments to all calls", function() {
            foo.setBar(123);
            expect(foo.setBar.calls.all()).toEqual([{object: foo, args: [123], returnValue: undefined}]);
        });

        //.calls.mostRecent(): 返回calls中追蹤的最近一次的請求數據;
        it("has a shortcut to the most recent call", function() {
            foo.setBar(123);
            foo.setBar(456, "baz");
            expect(foo.setBar.calls.mostRecent()).toEqual({object: foo, args: [456, "baz"], returnValue: undefined});
        });

        //.calls.first(): 返回calls中追蹤的第一次請求的數據;
        it("has a shortcut to the first call", function() {
            foo.setBar(123);
            foo.setBar(456, "baz");
            expect(foo.setBar.calls.first()).toEqual({object: foo, args: [123], returnValue: undefined});
        });

        //.object: 當調用all(),mostRecent(),first()方法時,返回對象的object屬性返回的是當前上下文對象;
        it("tracks the context", function() {
            var spy = jasmine.createSpy("spy");
            var baz = {
                fn: spy
            };
            var quux = {
                fn: spy
            };
            baz.fn(123);
            quux.fn(456);
            expect(spy.calls.first().object).toBe(baz);
            expect(spy.calls.mostRecent().object).toBe(quux);
        });

        //.calls.reset(): 重置Spy的所有追蹤數據;
        it("can be reset", function() {
            foo.setBar(123);
            foo.setBar(456, "baz");
            expect(foo.setBar.calls.any()).toBe(true);
            foo.setBar.calls.reset();
            expect(foo.setBar.calls.any()).toBe(false);
        });
    });

    /*
     jasmine.createSpy()
    * 假如沒有函數可以追蹤,我們可以自己創建一個空的Spy。
    * 創建后的Spy功能與其他的Spy一樣:跟蹤調用、參數等,但該Spy沒有實際的代碼實現,這種方式經常會用在對JavaScript中的對象的測試。
    * */
    describe("A spy, when created manually", function() {
        var whatAmI;

        beforeEach(function() {
            whatAmI = jasmine.createSpy("whatAmI");
            whatAmI("I", "am", "a", "spy");
        });

        it("is named, which helps in error reporting", function() {
            expect(whatAmI.and.identity()).toEqual("whatAmI");
        });

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

        it("tracks its number of calls", function() {
            expect(whatAmI.calls.count()).toEqual(1);
        });

        it("tracks all the arguments of its calls", function() {
            expect(whatAmI).toHaveBeenCalledWith("I", "am", "a", "spy");
        });

        it("allows access to the most recent call", function() {
            expect(whatAmI.calls.mostRecent().args[0]).toEqual("I");
        });
    });

    /*
     jasmine.createSpyObj()
     * 如果需要spy模擬多個函數調用,可以向jasmine.createSpyObj中傳入一個字符串數組,它將返回一個對象,
     * 你所傳入的所有字符串都將對應一個屬性,每個屬性即為一個Spy。
     * */
    describe("Multiple spies, when created manually", function() {
        var 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();
        });

        it("tracks that the spies were called", function() {
            expect(tape.play).toHaveBeenCalled();
            expect(tape.pause).toHaveBeenCalled();
            expect(tape.rewind).toHaveBeenCalled();
            expect(tape.stop).not.toHaveBeenCalled();
        });

        it("tracks all the arguments of its calls", function() {
            expect(tape.rewind).toHaveBeenCalledWith(0);
        });
    });

    /*
    *  jasmine.any()
    * 以構造器或者類名作為參數,Jasmine將判斷期望值和真實值的構造器是否相同,若相同則返回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() {
                var foo = jasmine.createSpy("foo");
                foo(12, function() {
                    return true;
                });
                expect(foo).toHaveBeenCalledWith(jasmine.any(Number), jasmine.any(Function));
            });
        });
    });

    /*
    * jasmine.anything()
    * 判斷只要不是null或undefined的值,若不是則返回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() {
                var foo = jasmine.createSpy('foo');
                foo(12, function() {
                    return false;
                });
                expect(foo).toHaveBeenCalledWith(12, jasmine.anything());
            });
        });
    });

    /*
    * jasmine.objectContaining()
    * 用來判斷對象中是否存在指定的鍵值屬性對。
    * */
    describe("jasmine.objectContaining", function() {
        var 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() {
                var callback = jasmine.createSpy("callback");
                callback({
                    bar: "baz"
                });
                expect(callback).toHaveBeenCalledWith(jasmine.objectContaining({
                    bar: "baz"
                }));
                expect(callback).not.toHaveBeenCalledWith(jasmine.objectContaining({
                    c: 37
                }));
            });
        });
    });

    /*
    * jasmine.arrayContaining()
    * 可以用來判斷數組中是否有期望的值。
    * */
    describe("jasmine.arrayContaining", function() {
        var foo;

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

        it("matches arrays with some of the values", function() {
            expect(foo).toEqual(jasmine.arrayContaining([3, 1]));  // 直接在期望值中使用jasmine.arrayContaining達到目的
            expect(foo).not.toEqual(jasmine.arrayContaining([6]));
        });

        describe("when used with a spy", function() {
            it("is useful when comparing arguments", function() {
                var callback = jasmine.createSpy("callback"); // 創建一個空的Spy
                callback([1, 2, 3, 4]); // 將數組內容作為參數傳入Spy中
                expect(callback).toHaveBeenCalledWith(jasmine.arrayContaining([4, 2, 3]));
                expect(callback).not.toHaveBeenCalledWith(jasmine.arrayContaining([5, 2]));
            });
        });
    });

    /*
    * jasmine.stringMatching()
    * 用來模糊匹配字符串,在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() {
                var callback = jasmine.createSpy("callback");
                callback("foobarbaz");
                expect(callback).toHaveBeenCalledWith(jasmine.stringMatching("bar"));
                expect(callback).not.toHaveBeenCalledWith(jasmine.stringMatching(/^bar$/));
            });
        });
    });

    /*
    * 不規則匹配(自定義匹配):asymmetricMatch
    * 某些場景下,我們希望能按照自己設計的規則進行匹配,此時我們可以自定義一個對象,該對象只要包含一個名為asymmetricMatch的方法即可。
    * */
    describe("custom asymmetry", function() {
        var tester = {
            asymmetricMatch: function(actual) {
                var 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() {
                var callback = jasmine.createSpy("callback");
                callback("foo,bar,baz");
                expect(callback).toHaveBeenCalledWith(tester);
            });
        });
    });

    /*
    * jasmine.clock()用來模擬操縱時間。
    * 要想使用jasmine.clock(),先調用jasmine.clock().install告訴Jasmine你想要在spec或者suite操作時間,當你不需要使用時,務必調用jasmine.clock().uninstall來恢復時間狀態。
    *
    * 示例中使用jasmine.clock().tick(milliseconds)來控制時間前進,本例中出現了三種時間控制方式:
    * setTimeout: 定期執行一次,當jasmine.clock().tick()的時間超過了timeout設置的時間時觸發
    * setInterval: 定期循環執行,每當jasmine.clock().tick()的時間超過了timeout設置的時間時觸發
    * mockDate: 模擬一個指定日期(當不提供基準時間參數時,以當前時間為基準時間)
    * */
    describe("Manually ticking the Jasmine Clock", function() {
        var timerCallback;

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

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

        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);
        });

        describe("Mocking the Date object", function(){
            it("mocks the Date object and sets it to a given time", function() {
                var baseTime = new Date();
                jasmine.clock().mockDate(baseTime);
                jasmine.clock().tick(50);
                expect(new Date().getTime()).toEqual(baseTime.getTime() + 50);
            });
        });
    });

    /*
    * Jasmine可以支持spec中執行異步操作。
    * 當調用beforeEach, it和afterEach時,函數可以包含一個可選參數done,當spec執行完畢之后,調用done通知Jasmine異步操作已執行完畢。
    *
    * 另外補充一點,如果需要設置全局的默認超時時間,可以設置jasmine.DEFAULT_TIMEOUT_INTERVAL的值,
    * 當異步執行時間超過設置的執行超時時間js將會報錯。
    * */
    describe("Asynchronous specs", function() {
        var value;

        beforeEach(function(done) {
            setTimeout(function() {
                value = 0;
                done();
            }, 1);
        });

        // 在上面beforeEach的done()被執行之前,這個測試用例不會被執行
        it("should support async execution of test preparation and expectations", function(done) {
            value++;
            expect(value).toBeGreaterThan(0);
            done(); // 執行完done()之后,該測試用例真正執行完成
        });

        // Jasmine異步執行超時時間默認為5秒,超過后將報錯
        describe("long asynchronous specs", function() {
            var originalTimeout;

            beforeEach(function() {
                originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
                // 設置全局的默認超時時間
                jasmine.DEFAULT_TIMEOUT_INTERVAL = 6000;
            });

            it("takes a long time", function(done) {
                setTimeout(function() {
                    done();
                }, 4000);
            });

            // 如果要調整指定用例的默認的超時時間,可以在beforeEach,it和afterEach中傳入一個時間參數
            //it("takes a long time for this spec", function(done) {
            //    setTimeout(function() {
            //        done();
            //    }, 6000);
            //}, 7000);

            afterEach(function() {
                jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
            });
        });
    });
}());
image
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言 本篇文章是我在學習前端自動化單元測試時的一些思路整理,之前也從未接觸過單元測試相關工具,如有錯漏,請讀者斧正...
    Awey閱讀 12,696評論 8 37
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,372評論 25 708
  • JavaScript 資源大全中文版很多程序員應該記得 GitHub 上有一個 Awesome - XXX 系列的...
    wwmin_閱讀 3,503評論 1 91
  • 人生一但鎖定了目標,就要以實際行動努力去實現。期間可能會遇到很多挫折,但我相信,只要通過自己不懈的努力,就一定會有...
    淡看人間事閱讀 525評論 0 0
  • 1.引用類型有哪些?非引用類型有哪些? 數值、字符串、布爾值、null和undefined為基本類型值,這些都是保...
    Schrodinger的貓閱讀 191評論 0 0