AngularJs單元測試

eo2485 8年前發布 | 14K 次閱讀 單元測試 Web框架 angularjs

來自: http://www.cnblogs.com/laogai/p/5225234.html

這篇文章主要介紹了angularJS中的單元測試實例,本文主要介紹利用Karma和Jasmine來進行ng模塊的單元測試,并用Istanbul  來生成代碼覆蓋率測試報告,需要的朋友們可以參考下,以下可全都是干貨哦!

當ng項目越來越大的時候,單元測試就要提上日程了,有的時候團隊是以測試先行,有的是先實現功能,后面再測試功能模塊,這個各有利弊,今天主要說說利用karma和jasmine來進行ng模塊的單元測試.

一、Karma+Jasmine+ Istanbul

Karma是Testacular的新名字,在2012年google開源了Testacular,2013年Testacular改名為Karma.Karma是一個讓人感到非常神秘的名字,表示佛教中的緣分,因果報應,比Cassandra這種名字更讓人猜不透!

Karma是一個基于Node.js的JavaScript測試執行過程管理工具(Test Runner).該工具可用于測試所有主流Web瀏覽器,也可集成到CI(Continuous integration)工具,也可和其他代碼編輯器一起使用.這個測試工具的一個強大特性就是,它可以監控(Watch)文件的變化,然后自行執行,通過console.log顯示測試結果.

Jasmine is a behavior-driven development framework for testing JavaScript code. It does not depend on any other JavaScript frameworks. It does not require a DOM. And it has a clean, obvious syntax so that you can easily write tests.

上面是Jasmine官網對其的解釋,中文意思是:Jasmine是一個用于JS代碼測試的行為驅動開發的框架,它不依賴于任何其他的JS框架以及DOM,是一個簡潔及友好的API測試庫.

Istanbul是JavaScript程序的代碼覆蓋率工具,它是以土耳其最大城市伊斯坦布爾命名,因為土耳其地毯世界聞名,而地毯是用來覆蓋的.

二、Karma的安裝

安裝測試相關的npm模塊建議使用—save-dev命令,因為這是開發相關的,一般情況下使用以下兩個命令即可:

npm install  karma –save-dev

檢測karma安裝是否成功(如下表示安裝成功):

安裝karma時會自動安裝一些常用的模塊,參考karma代碼里的package.json文件的devDependencies屬性:

"devDependencies": {

"karma-browserify": "^5.0.1",

"karma-browserstack-launcher": "^0.1.10",

"karma-chrome-launcher": "*",

"karma-coffee-preprocessor": "*",

"karma-commonjs": "*",

"karma-coverage": "*",

"karma-firefox-launcher": "*",

"karma-growl-reporter": "*",

"karma-html2js-preprocessor": "*",

"karma-jasmine": "~0.3.5",

"karma-junit-reporter": "*",

"karma-live-preprocessor": "*",

"karma-mocha": "0.2.1",

"karma-ng-scenario": "*",

"karma-phantomjs-launcher": "*",

"karma-qunit": "*",

"karma-requirejs": "*",

"karma-sauce-launcher": "*",

"karma-script-launcher": "^0.1.0"</pre>

}

然后使用命令生成配置文件,該配置文件是nodejs風格的:

命令:karma init

輸入命令后根據提示,使用“tab”切換選項和“enter”下一步即可,生成的配置文件格式如下:

// Karma configuration

// Generated on Wed Feb 24 2016 16:18:27 GMT+0800 (中國標準時間)module.exports = function(config) {

config.set({

//files中文件的基礎目錄;

basePath: '',

// 應用的測試框架;

frameworks: ['jasmine'],

// 測試環境中需要加載的js文件;

files: [

    'public/bower_components/angular/angular.js',

    'public/bower_components/angular-ui-router/release/angular-ui-router.js',

    'public/bower_components/angular-mocks/angular-mocks.js',

    'public/src/angularRoute.js',

    'public/src/controller/*.js',

    'public/testjs/test/*.js'

],

//  需要執行的文件列表;

exclude: [

    'karma.Conf.js'

],

  // 添加插件;

  plugins: [ 'karma-jasmine', 'karma-chrome-launcher', 'karma-coverage'],

// 測試需要引入的模塊名;

reporters: ['progress','coverage'],



  // preprocess matching files before serving them to the browser

  // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor

  preprocessors: {

      'public/src/controller/IndexCtrl.js':['coverage']

  },

  // 代碼覆蓋輸出配置;

  coverageReporter:{

      type:'html',

      dir:'coverage/'

  },

// 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: <em>config</em>.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

})

}</pre>

以上便是karma配置文件的基本內容.

四、Karma+Jasmine配置

1、安裝karma-jasmine:

使用命令 : npm install karma-jasmine --save-dev 安裝

2、jasmine的語法:

以下為一個jasmine的例子:

describe("A spy, when configured with an alternate implementation", function() {

var foo, bar, fetchedBar;

beforeEach(function() {

    foo = {

        setBar: function(<em>value</em>) {

            bar = <em>value</em>;

        },

        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() {

    <strong><em>console</em></strong>.log("foo.getBar="+foo.getBar());

    <strong><em>expect</em></strong>(foo.getBar).toHaveBeenCalled();

});

it("should not affect other functions", function() {

    <strong><em>console</em></strong>.log("bar="+bar);

    <strong><em>expect</em></strong>(bar).toEqual(123);

});

it("when called returns the requested value", function() {

    <strong><em>console</em></strong>.log("fetchedBar="+fetchedBar);

    <strong><em>expect</em></strong>(fetchedBar).toEqual(1001);

    <strong><em>expect</em></strong>(1).not.toBe(2);

    <strong><em>expect</em></strong>({}).not.toBe({});//toBE相當于=== (全等,包括對象類型)

    <strong><em>expect</em></strong>({}).toEqual({});//toEqual相當于 == (等于,只關心大小,不關心類型)

});

});</pre>

上面是一個jasmine的例子,這里就幾個重要的API做一下介紹:

1.首先任何一個測試用例以describe函數來定義,它有兩參數,第一個用來描述測試大體的中心內容,第二個參數是一個函數,里面寫一些真實的測試代碼

2.it是用來定義單個具體測試任務,也有兩個參數,第一個用來描述測試內容,第二個參數是一個函數,里面存放一些測試方法

3.expect主要用來計算一個變量或者一個表達式的值,然后用來跟期望的值比較或者做一些其它的事件

4.beforeEach與afterEach主要是用來在執行測試任務之前和之后做一些事情,上面的例子就是在執行之前改變變量的值,然后在執行完成之后重置變量的值

最后要說的是,describe函數里的作用域跟普通JS一樣都是可以在里面的子函數里訪問的,就像上面的it訪問foo變量,更多的API請 點擊這里 .

五、ng的單元測試

因為ng本身框架的原因,模塊都是通過DI(Dependency Injection依賴注入)來加載以及實例化的,所以為了方便配合jasmine來編寫測試腳本,所以官方提供了angular-mock.js的一個測試工具類來提供模塊定義,加載,注入等.

下面說說ng-mock里的一些常用方法

1.angular.mock.module 此方法同樣在window命名空間下,非常方便調用module是用來配置inject方法注入的模塊信息,參數可以是字符串,函數,對象,可以像下面這樣使用,代碼如下:

beforeEach(module('myApp.filters'));

beforeEach(module(function($provide) {

<em>$provide</em>.value('version', 'TEST_VER');

}));</pre>

它一般用在beforeEach方法里,因為這個可以確保在執行測試任務的時候,inject方法可以獲取到模塊配置

2.angular.mock.inject 此方法同樣在window命名空間下,非常方便調用inject是用來注入上面配置好的ng模塊,方面在it的測試函數里調用,常見的調用例子如下:

angular.module('myApplicationModule', [])

.value('mode', 'app')

.value('version', 'v1.0.1');



describe('MyApp', function() {

// You need to load modules that you want to test,

// it loads only the "ng" module by default.

beforeEach(<strong><em>module</em></strong>('myApplicationModule'));



// inject() is used to inject arguments of all given functions

it('should provide a version', inject(function(<em>mode</em>, <em>version</em>) {

    <strong><em>expect</em></strong>(<em>version</em>).toEqual('v1.0.1');

    <strong><em>expect</em></strong>(<em>mode</em>).toEqual('app');

}));

// The inject and module method can also be used inside of the it or beforeEach

it('should override a version and test the new version is injected', function() {

    // module() takes functions or strings (module aliases)

    <strong><em>module</em></strong>(function(<em>$provide</em>) {

        <em>$provide</em>.value('version', 'overridden'); // override version here

    });

    inject(function(<em>version</em>) {

        <strong><em>expect</em></strong>(<em>version</em>).toEqual('overridden');

    });

});

});</pre>

上面是官方提供的一些inject例子,代碼很好看懂,其實inject里面就是利用angular.inject方法創建的一個內置的依賴注入實例,然后里面的模塊注入跟普通ng模塊里的依賴處理是一樣的.

簡單的介紹完ng-mock之后,下面我們分別以控制器,指令,過濾器來編寫一個簡單的單元測試.

3、ng里控制器的單元測試

定義一個簡單的控制器, 代碼如下:

angular.module("app")

.controller("indexCtrl",['$scope',function(<em>$scope</em>){

    <em>$scope</em>.user={

        name:'呼呼',

        age:'13'

    };

}])</pre> 

然后我們編寫一個測試腳本,代碼如下:

//測試控制器;

describe("測試home頁對應的控制器",function(){

describe("測試indexContrl",function(){

   var $scope;

   beforeEach(<strong><em>module</em></strong>('app'));

   beforeEach(inject(function(<em>$rootScope</em>,<em>$controller</em>){

       $scope=<em>$rootScope</em>.$new();

       <em>$controller</em>('indexCtrl',{$scope:$scope});

   }));

   it("測試user對象的名稱是否為空",function(){

       <strong><em>expect</em></strong>($scope.user.name).not.toBeNull();

   });

   it("測試user對象的年齡是否合法",function(){

      <strong><em>expect</em></strong>($scope.user.age).toMatch(/^[1-9][0-9]{0,2}/);

   });

});

});</pre>

上面利用了$rootScope來創建子作用域,然后把這個參數傳進控制器的構建方法$controller里去,最終會執行上面的控制器里的方法,然后我們檢查子作用域里的對象屬性是否跟期望值相等.

4、ng里指令的單元測試

定義一個簡單的指令,代碼如下:

angular.module("app")

.directive('aGreatEye',function(){

    return {

        restrict:'E',

        replace:true,

        template:'<h1>這是標題</h1>'

    }

})</pre> 

然后我們編寫一個簡單的測試腳本,代碼如下:

//測試directive指令;

describe("測試指令",function(){

var $compile;

var $rootScope;

beforeEach(<strong><em>module</em></strong>('app'));



/*

inject注入,前后加'_',最后會被ng處理掉;

*/

beforeEach(inject(function(<em>_$compile_</em>,<em>_$rootScope_</em>){

    $compile=<em>_$compile_</em>;

    $rootScope=<em>_$rootScope_</em>;

}));

it("測試指令aGreatEye",function(){

    //創建dom元素;

    var element=$compile('<a-great-eye></a-great-eye>')($rootScope);//compile傳入指令html,在返回的函數里傳入rootScope后,完成試圖與作用域的綁定;

    $rootScope.$digest();//觸發所有的監聽;

    <strong><em>expect</em></strong>(element.html()).toContain('標題');

});

});</pre>

上面的指令將會這用在html里使用,代碼如下:

<a-great-eye></a-great-eye>

測試腳本里首先注入$compile與$rootScope兩個服務,一個用來編譯html,一個用來創建作用域用,注意這里的_,默認ng里注入的服務前后加上_時,最后會被ng處理掉的,這兩個服務保存在內部的兩個變量里,方便下面的測試用例能調用到$compile方法傳入原指令html,然后在返回的函數里傳入$rootScope,這樣就完成了作用域與視圖的綁定,最后調用$rootScope.$digest來觸發所有監聽,保證視圖里的模型內容得到更新,然后獲取當前指令對應元素的html內容與期望值進行對比.

5、ng里的過濾器單元測試

定義一個簡單的過濾器,代碼如下:

angular.module("app")
.filter('interpolate',['version',function(version){

return function (<em>text</em>){

    //if(text=='AAA') return 'BBB';

    //返回:找到一個或多個(可換行多匹配)‘%VERSION%’,將這些字符串全部換為參數version中的內容;

    return String(<em>text</em>).replace(/\%VERSION\%/mg, <em>version</em>);

}

}]);</pre>

然后編寫一個簡單的測試腳本,代碼如下:

//測試filter過濾器;

describe("測試過濾器",function(){

beforeEach(<strong><em>module</em></strong>('app'));

describe('interpolate',function(){

   beforeEach(<strong><em>module</em></strong>(function(<em>$provide</em>){

       <em>$provide</em>.value('version','TEST_VER');//設置給version賦值為‘TEST_VER’;

   }));

    it("應該替換VERSION",inject(function(<em>interpolateFilter</em>){

        //interpolate('before %VERSION% after')意思是調用過濾器,結果為‘before TEST_VER after’;

        <strong><em>expect</em></strong>(<em>interpolateFilter</em>('before %VERSION% after')).toEqual('before TEST_VER after');

    }));

});

});</pre>

上面的代碼先配置過濾器模塊,然后定義一個version值,因為interpolate依賴這個服務,最后用inject注入interpolate過濾器,注意這里的過濾器后面得加上Filter后綴,最后傳入文本內容到過濾器函數里執行,與期望值進行對比.

最終上面的被測試文件IndexCtrl.js代碼如下:

 "use strict";

angular.module("app")

.controller("indexCtrl",['$scope',function(<em>$scope</em>){

    <em>$scope</em>.user={

        name:'呼呼',

        age:'13'

    };

}])

.directive('aGreatEye',function(){

    return {

        restrict:'E',

        replace:true,

        template:'<h1>這是標題</h1>'

    }

})

.filter('interpolate',['version',function(<em>version</em>){

    return function (<em>text</em>){

        //返回:找到一個或多個(可換行多匹配)‘%VERSION%’,將這些字符串全部換為參數version中的內容;

        return String(<em>text</em>).replace(/\%VERSION\%/mg, <em>version</em>);

    }

}]);</pre> 

測試腳本IndexCtrlTest.js代碼如下:

//測試控制器;

describe("測試home頁對應的控制器",function(){

describe("測試indexContrl",function(){

   var $scope;

   beforeEach(<strong><em>module</em></strong>('app'));

   beforeEach(inject(function(<em>$rootScope</em>,<em>$controller</em>){

       $scope=<em>$rootScope</em>.$new();

       <em>$controller</em>('indexCtrl',{$scope:$scope});

   }));

   it("測試user對象的名稱是否為空",function(){

       <strong><em>expect</em></strong>($scope.user.name).not.toBeNull();

   });

   it("測試user對象的年齡是否合法",function(){

      <strong><em>expect</em></strong>($scope.user.age).toMatch(/^[1-9][0-9]{0,2}/);

   });

});

});

//測試directive指令;

describe("測試指令",function(){

var $compile;

var $rootScope;

beforeEach(<strong><em>module</em></strong>('app'));



/*

inject注入,前后加'_',最后會被ng處理掉;

*/

beforeEach(inject(function(<em>_$compile_</em>,<em>_$rootScope_</em>){

    $compile=<em>_$compile_</em>;

    $rootScope=<em>_$rootScope_</em>;

}));

it("測試指令aGreatEye",function(){

    //創建dom元素;

    var element=$compile('<a-great-eye></a-great-eye>')($rootScope);//compile傳入指令html,在返回的函數里傳入rootScope后,完成試圖與作用域的綁定;

    $rootScope.$digest();//觸發所有的監聽;

    <strong><em>expect</em></strong>(element.html()).toContain('標題');

});

});

//測試filter過濾器;

describe("測試過濾器",function(){

beforeEach(<strong><em>module</em></strong>('app'));

describe('interpolate',function(){

   beforeEach(<strong><em>module</em></strong>(function(<em>$provide</em>){

       <em>$provide</em>.value('version','TEST_VER');//設置給version賦值為‘TEST_VER’;

   }));

    it("應該替換VERSION",inject(function(<em>interpolateFilter</em>){

        //interpolate('before %VERSION% after')意思是調用過濾器,結果為‘before TEST_VER after’;

        <strong><em>expect</em></strong>(<em>interpolateFilter</em>('before %VERSION% after')).toEqual('before TEST_VER after');

    }));

});

});</pre>

六、運行

輸入命令: karma start karma.conf.js 啟動,測試結果如下:

七、Karma+Istanbul 生成代碼覆蓋率

安裝istanbul依賴:npm install –g karma-coverage

修改karma.conf.js配置:

啟動karma,在工程目錄下找到coverage文件夾,生成的覆蓋率文件都包含在該文件夾中,在瀏覽器中打開“coverage/index.html”文件,可看到生成的代碼覆蓋率報告:

覆蓋率是100%,說明我們完整了測試了IndexCtrl.js的功能.

現在我們在filter中加入一個if分支,代碼如下:

.filter('interpolate',['version',function(version){

return function (<em>text</em>){

    if(<em>text</em>=='AAA') return 'BBB';

    //返回:找到一個或多個(可換行多匹配)‘%VERSION%’,將這些字符串全部換為參數version中的內容;

    return String(<em>text</em>).replace(/\%VERSION\%/mg, <em>version</em>);

}

}]);</pre>

再看代碼覆蓋率報告,如下所示:

Statements:85.71%覆蓋,Branches:50%覆蓋.

為了產品的質量我們要盡量達到更多的覆蓋率,一般對于JAVA項目來說,能達到80%就是相當高的標準了.對于Javascript的代碼測試及覆蓋率研究,我還要做更多的驗證.

PS:希望廣大讀者朋友批評指證,如需轉載請注明出處.

</div>

 本文由用戶 eo2485 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!