當ng項目越來越大的時候,單元測試就要提上日程了,有的時候團隊是以測試先行,有的是先實現功能,後面再測試功能模塊,這個各有利弊,今天主要說說利用karma和jasmine來進行ng模塊的單元測試.
什麼是Karma
karma是一個單元測試的運行控制框架,提供以不同環境來運行單元測試,比如chrome,firfox,phantomjs等,測試框架支持jasmine,mocha,qunit,是一個以nodejs為環境的npm模塊.
安裝測試相關的npm模塊建議使用----save-dev參數,因為這是開發相關的,一般的運行karma的話只需要下面兩個npm命令
代碼如下:
npm install karma --save-dev
npm install karma-junit-reporter --save-dev
安裝karma的時候會自動的安裝一些常用的模塊,參考karma代碼裡的package.json文件的peerDependencies屬性
代碼如下:
"peerDependencies": {
"karma-jasmine": "~0.1.0",
"karma-requirejs": "~0.2.0",
"karma-coffee-preprocessor": "~0.1.0",
"karma-html2js-preprocessor": "~0.1.0",
"karma-chrome-launcher": "~0.1.0",
"karma-firefox-launcher": "~0.1.0",
"karma-phantomjs-launcher": "~0.1.0",
"karma-script-launcher": "~0.1.0"
}
然後一個典型的運行框架通常都需要一個配置文件,在karma裡可以是一個karma.conf.js,裡面的代碼是一個nodejs風格的,一個普通的例子如下:
代碼如下:
module.exports = function(config){
config.set({
// 下面files裡的基礎目錄
basePath : '../',
// 測試環境需要加載的JS信息
files : [
'app/bower_components/angular/angular.js',
'app/bower_components/angular-route/angular-route.js',
'app/bower_components/angular-mocks/angular-mocks.js',
'app/js/**/*.js',
'test/unit/**/*.js'
],
// 是否自動監聽上面文件的改變自動運行測試
autoWatch : true,
// 應用的測試框架
frameworks: ['jasmine'],
// 用什麼環境測試代碼,這裡是chrome`
browsers : ['Chrome'],
// 用到的插件,比如chrome浏覽器與jasmine插件
plugins : [
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-jasmine',
'karma-junit-reporter'
],
// 測試內容的輸出以及導出用的模塊名
reporters: ['progress', 'junit'],
// 設置輸出測試內容文件的信息
junitReporter : {
outputFile: 'test_out/unit.xml',
suite: 'unit'
}
});
};
這裡要注意的時,上面的插件大部分都不需要單獨安裝,因為安裝karma的時候已經安裝了,這裡只有karma-junit-reporter導出插件需要單獨安裝,想要了解更多的關於配置文件的信息可以,點擊這裡
karma就講到這裡,想了解更多關於它的信息可以,點擊這裡
什麼是jasmine
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框架以及dom,是一個非常干淨以及友好API的測試庫.
下面簡單的以一個例子來說明它的用法
定義一個測試文件命令為test.js
代碼如下:
describe("A spec (with setup and tear-down)", function() {
var 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);
});
});
上面的例子來自於官網,這裡只說下幾個重要的API,更多的用法請,點擊這裡
1.首先任何一個測試用例以describe函數來定義,它有兩參數,第一個用來描述測試大體的中心內容,第二個參數是一個函數,裡面寫一些真實的測試代碼
2.it是用來定義單個具體測試任務,也有兩個參數,第一個用來描述測試內容,第二個參數是一個函數,裡面存放一些測試方法
3.expect主要用來計算一個變量或者一個表達式的值,然後用來跟期望的值比較或者做一些其它的事件
4.beforeEach與afterEach主要是用來在執行測試任務之前和之後做一些事情,上面的例子就是在執行之前改變變量的值,然後在執行完成之後重置變量的值
最後要說的是,describe函數裡的作用域跟普通JS一樣都是可以在裡面的子函數裡訪問的,就像上面的it訪問foo變量
想要運行上面的測試例子可以通過karar來運行,命令例子如下:
代碼如下:
karma start test/karma.conf.js
下面我們重點的說說ng裡的控制器,指令,服務模塊的單元測試.
NG的單元測試
因為ng本身框架的原因,模塊都是通過di來加載以及實例化的,所以為了方便配合jasmine來編寫測試腳本,所以官方提供了angular-mock.js的一個測試工具類來提供模塊定義,加載,注入等.
下面說說ng-mock裡的一些常用方法
1.angular.mock.module 此方法同樣在window命名空間下,非常方便調用
module是用來配置inject方法注入的模塊信息,參數可以是字符串,函數,對象,可以像下面這樣使用
代碼如下:
beforeEach(module('myApp.filters'));
beforeEach(module(function($provide) {
$provide.value('version', 'TEST_VER');
}));
它一般用在beforeEach方法裡,因為這個可以確保在執行測試任務的時候,inject方法可以獲取到模塊配置
1.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(module('myApplicationModule'));
// inject() is used to inject arguments of all given functions
it('should provide a version', inject(function(mode, version) {
expect(version).toEqual('v1.0.1');
expect(mode).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)
module(function($provide) {
$provide.value('version', 'overridden'); // override version here
});
inject(function(version) {
expect(version).toEqual('overridden');
});
});
});
上面是官方提供的一些inject例子,代碼很好看懂,其實inject裡面就是利用angular.inject方法創建的一個內置的依賴注入實例,然後裡面的模塊注入跟普通ng模塊裡的依賴處理是一樣的
簡單的介紹完ng-mock之後,下面我們分別以控制器,指令,過濾器來編寫一個簡單的單元測試.
ng裡控制器的單元測試
定義一個簡單的控制器
代碼如下:
var myApp = angular.module('myApp',[]);
myApp.controller('MyController', function($scope) {
$scope.spices = [{"name":"pasilla", "spiciness":"mild"},
{"name":"jalapeno", "spiciness":"hot hot hot!"},
{"name":"habanero", "spiciness":"LAVA HOT!!"}];
$scope.spice = "hello feenan!";
});
然後我們編寫一個測試腳本
代碼如下:
describe('myController function', function() {
describe('myController', function() {
var $scope;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
$controller('MyController', {$scope: $scope});
}));
it('should create "spices" model with 3 spices', function() {
expect($scope.spices.length).toBe(3);
});
it('should set the default value of spice', function() {
expect($scope.spice).toBe('hello feenan!');
});
});
});
上面利用了$rootScope來創建子作用域,然後把這個參數傳進控制器的構建方法$controller裡去,最終會執行上面的控制器裡的方法,然後我們檢查子作用域裡的數組數量以及字符串變量是否跟期望的值相等.
想要了解更多關於ng裡的控制器的信息,可以點擊這裡
ng裡指令的單元測試
定義一個簡單的指令
代碼如下:
var app = angular.module('myApp', []);
app.directive('aGreatEye', function () {
return {
restrict: 'E',
replace: true,
template: '<h1>lidless, wreathed in flame, 1 times</h1>'
};
});
然後我們編寫一個簡單的測試腳本
代碼如下:
describe('Unit testing great quotes', function() {
var $compile;
var $rootScope;
// Load the myApp module, which contains the directive
beforeEach(module('myApp'));
// Store references to $rootScope and $compile
// so they are available to all tests in this describe block
beforeEach(inject(function(_$compile_, _$rootScope_){
// The injector unwraps the underscores (_) from around the parameter names when matching
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('Replaces the element with the appropriate content', function() {
// Compile a piece of HTML containing the directive
var element = $compile("<a-great-eye></a-great-eye>")($rootScope);
// fire all the watches, so the scope expression 1 will be evaluated
$rootScope.$digest();
// Check that the compiled element contains the templated content
expect(element.html()).toContain("lidless, wreathed in flame, 2 times");
});
});
上面的例子來自於官方提供的,最終上面的指令將會這用在html裡使用
代碼如下:
<a-great-eye></a-great-eye>
測試腳本裡首先注入$compile與$rootScope兩個服務,一個用來編譯html,一個用來創建作用域用,注意這裡的_,默認ng裡注入的服務前後加上_時,最後會被ng處理掉的,這兩個服務保存在內部的兩個變量裡,方便下面的測試用例能調用到
$compile方法傳入原指令html,然後在返回的函數裡傳入$rootScope,這樣就完成了作用域與視圖的綁定,最後調用$rootScope.$digest來觸發所有監聽,保證視圖裡的模型內容得到更新
然後獲取當前指令對應元素的html內容與期望值進行對比.
想要了解更多關於ng裡的指令的信息,可以點擊這裡
ng裡的過濾器單元測試
定義一個簡單的過濾器
代碼如下:
var app = angular.module('myApp', []);
app.filter('interpolate', ['version', function(version) {
return function(text) {
return String(text).replace(/\%VERSION\%/mg, version);
};
}]);
然後編寫一個簡單的測試腳本
代碼如下:
describe('filter', function() {
beforeEach(module('myApp'));
describe('interpolate', function() {
beforeEach(module(function($provide) {
$provide.value('version', 'TEST_VER');
}));
it('should replace VERSION', inject(function(interpolateFilter) {
expect(interpolateFilter('before %VERSION% after')).toEqual('before TEST_VER after');
}));
});
});
上面的代碼先配置過濾器模塊,然後定義一個version值,因為interpolate依賴這個服務,最後用inject注入interpolate過濾器,注意這裡的過濾器後面得加上Filter後綴,最後傳入文本內容到過濾器函數裡執行,與期望值進行對比.
總結
利用測試來開發NG有很多好處,可以保證模塊的穩定性,還有一點就是能夠深入的了解ng的內部運行機制,所以建議用ng開發的同學趕緊把測試補上吧!