自動測試是AngularJS架構的重要一環,官方文件有專章討論單元測試,程式庫則有ngMock提供單元測試所時的DI及Mocking(假物件模擬)支援。本篇將討論如何在Visual Studio 2013對Controller及ViewModel進行單元測試。

開始之前,Visual Studio要先安裝Chutzpah(發音類似"鬍子爸" XD)延伸套件,流浪小風有篇詳細介紹,在此不多贅述,直接切入Angular相關部分。我們就拿NG筆記2的簡單範例來練兵,為它建立單元測試。

首先,在Solution新增一個Class Libaray專案,並從NuGet安裝jasmine.js。(Jasmine是一套BDD精神的JavaScript測試Framework,AngularJS的單元測試及End-to-End(E2E)測試都用Jasmine寫測試腳本)

接著,在專案新增Tests資料夾,專門用來放置測試Script,測試檔可使用*.tests.js格式命名。整體的專案結構如下:

注意,T1.tests.js需以<reference>引用來自Web專案裡的JavaScript,Chutzpah會依照reference載入所需JS執行單元測試。如果希望編寫Jasmine Script階段能有完整Intellisense,可以連angular-mock.js、jasmine.js也一併納入參考。

T1.tests.js範例如下:

//測試集
describe("SampleApp測試", function () {
    var scope, controller;
    //beforeEach()內為每回測試前執行的程序
    beforeEach(function () {
        //透過ngMock註冊sampleApp的相關設定,供隨後DI產生模組
        module("sampleApp");
    });
 
    //子測試集
    describe("mainCtrl測試", function () {
        //每個Spec(下方的it())執行前使用ngMock inject()產生Controller及Scope
        beforeEach(inject(function ($rootScope, $controller) {
            scope = $rootScope.$new();
            controller = $controller("mainCtrl", {
                "$scope": scope
            });
        }));
        //Spec 1
        it("初始firstName等於'Jeffrey'", function () {
            expect(scope.model.firstName).toBe("Jeffrey");
        });
        //Spec 2
        it("fullName()由firstName及lastName組成", function () {
            scope.model.firstName = "Darkthread";
            scope.model.lastName = "Run"
            expect(scope.model.fullName()).toBe("Darkthread Run");
        });
    });
});

上述程式中的describe(), it(), expect()是Jasmine指令,Jasmine的指令沒幾個,官方文件看一遍就能上手,甚至不讀文件,直接看程式碼也不難望文生義。程式裡的module()、inject()是ngMock提供的功能,處理Module、Controller載入及Scope建立,透過DI可依需要注入假物件,而ngMock提供的$httpBackend假物件能模擬AJAX呼叫行為,確保前端程式在沒有伺服器時能也能單獨執行,這是形成單元測試的重要條件。每個it()通常用來印證一條"規格"、beforeEach()內的程式在每次跑it()前會先被執行,備妥測試環境。

Visual Studio安裝Chutzpah套件後,在T1.tests.js按右鍵可選擇直接執行測試或開啟瀏覽器檢視測試報表: (瀏覽器檢視時還能用Dev Tools偵錯,實務上不可或缺)

或者用Test Explorer選取測試執行也成:

介紹完用tests.js跑測試,該來談談TypeScript時代的單元測試,看看如何用TypeScript寫Jasmine測試?

首先在測試專案加入Jasmine的TypeScript定義檔:

新增TypeScript測試檔(T2.tests.ts),內容如下:

/// <reference path="../scripts/typings/jasmine/jasmine.d.ts" />
/// <reference path="../../web/scripts/typings/angularjs/angular-mocks.d.ts" />
/// <reference path="../../web/scripts/vms/user.ts" />
/// <reference path="../../web/scripts/apps/sampleapp.ts" />
 
/// <chutzpah_reference path="../../web/scripts/boo.js" />
/// <chutzpah_reference path="../../web/scripts/angular.js" />
/// <chutzpah_reference path="../../web/scripts/angular-mocks.js" />
/// <chutzpah_reference path="../../web/scripts/vms/users.js" />
/// <chutzpah_reference path="../../web/scripts/apps/sampleApp.js" />
 
describe("[TS]SampleApp測試", () => {
    var scope, controller;
    beforeEach(module("sampleApp"));
    beforeEach(inject(function ($rootScope, $controller) {
        scope = $rootScope.$new();
        controller = $controller("mainCtrl", {
            "$scope": scope
        });
    }));
    
    it("初始firstName等於'Jeffrey'", () => {
        expect(scope.model.firstName).toBe("Jeffrey");
    });
    it("fullName()由firstName及lastName組成", () => {
        var model: User = scope.model;
        model.firstName = "Darkthread";
        model.lastName = "Run"
        expect(model.fullName()).toBe("Darkthread Run");
    });
});

重點在上方的參考宣告。測試TypeScript會用到Jasmine的describe(), beforeEach(),也會用到ngMock的module(), inject(),因此要納入jasmine.d.ts及angular-mocks.d.ts參考才能編譯,而測試要用到的Controller及ViewModel TypeScript,也需以<reference>加入參考。

TypeScript測試檔跟JavaScript測試檔的最大不同,在於<reference>加入*.ts是要讓TypeScript順利編譯,不等於執行期間要載入的JS項目,因此需要另外定義<chutzpah_reference>,取代原本JavaScript <reference>,讓Chutzpah知道要跑測試時載入哪些JavaScript檔案。所以在撰寫TypeScript測試檔時,除了參考TS,不要忘了用<chutzpah_reference>將需要的JS都列上去。

T2.tests.ts完成後,透過右鍵Run Tests及Test Exploer就能執行TypeScript所寫的測試檔。

提醒: 使用TypeScript時,不建議使用右鍵"Open in browser"功能,原因是Chutzpah每次執行會重新編譯TypeScript,產生類似_Chutzpah.53.sampleapp.js的暫存檔,為配合瀏覽器開啟檔案不會自動刪除,久而久之就會產生一大堆暫存檔,要改善這個問題,建議使用自訂TestRunner網頁取代。

[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

Comments

Be the first to post a comment

Post a comment


66 + 18 =