AngularJS測試
一 測試工具
1.NodeJS領域:Jasmine做單元測試,Karma自動化完成單元測試,Grunt啟動Karma統一項目管理,Yeoman最后封裝成一個項目原型模板,npm做nodejs的包依賴管理,bower做javascript的包依賴管理。Java領域:JUnit做單元測試, Maven自動化單元測試,統一項目管理,構建項目原型模板,包依賴管理。
Nodejs讓組合變得更豐富,卻又在加重我們的學習門檻。唉......
2.Karma
Karma是一個測試工具,它從頭開始構建,免去了設置測試方面的負擔,這樣我們就可以將主要精力放在構建核心應用邏輯上。
Karma產生一個瀏覽器實例(或者多個不同的瀏覽器實例),針對不同的瀏覽器實例運行測試,檢測在不同瀏覽器環境下測試是否通過。Karma與瀏覽器通過socket.io來聯系,這能讓Karma保持持續通信。因此Karma提供了關於哪些測試正在運行的實時反饋,提供一份適合人類閱讀的輸出,告訴我們哪些測試通過、哪些失敗或者超時。
Karma產生一個瀏覽器實例(或者多個不同的瀏覽器實例),針對不同的瀏覽器實例運行測試,檢測在不同瀏覽器環境下測試是否通過。Karma與瀏覽器通過socket.io來聯系,這能讓Karma保持持續通信。因此Karma提供了關於哪些測試正在運行的實時反饋,提供一份適合人類閱讀的輸出,告訴我們哪些測試通過、哪些失敗或者超時。
Karma測試運行器同時支持單元測試和端到端測試。
3.Karma安裝
如果你已經安裝了NodeJS和npm,就可以通過npm命令來安裝karma
安裝命令:npm install -g karma
測試是否安裝成功:karma start
如果成功安裝karma,則可以通過瀏覽器看到karma界面
4.karma+jasmine配置
初始化karma配置文件karma.conf.js:karma init
安裝集成包karma-jasmine:npm install karma-jasmine
karma安裝好后,后面就是具體的jasmine測試了。
二 jasmine測試
1.盡管Karma支持多種測試框架,但默認的選項是Jasmine。Jasmine是一個用於測試JavaScript代碼的行為驅動開發框架。
下載發布的安裝包:jasmine-standalone-2.2.0.zip
下載后的目錄結構:
(1)lib:存放了運行測試案例所必須的文件,其內包含jasmine-2.2.0文件夾。可以將不同版本的Jasmine放在lib下,以便使用時切換。
jasmine.js:整個框架的核心代碼。
jasmine-html.js:用來展示測試結果的js文件。
boot.js:jasmine框架的的啟動腳本。需要注意的是,這個腳本應該放在jasmine.js之后,自己的js測試代碼之前加載。
jasmine.css:用來美化測試結果。
boot.js:jasmine框架的的啟動腳本。需要注意的是,這個腳本應該放在jasmine.js之后,自己的js測試代碼之前加載。
jasmine.css:用來美化測試結果。
(2)spec:存放測試腳本。
PlayerSpec.js:就是針對src文件夾下的Player.js所寫的測試用例。
SpecHelper.js:用來添加自定義的檢驗規則,如果框架本身提供的規則(諸如toBe,toNotBe等)不適用,就可以額外添加自己的規則(在本文件中添加了自定義的規則toBePlaying)。
SpecHelper.js:用來添加自定義的檢驗規則,如果框架本身提供的規則(諸如toBe,toNotBe等)不適用,就可以額外添加自己的規則(在本文件中添加了自定義的規則toBePlaying)。
(3)src:存放需要測試的js文件。Jasmine提供了一個Example(Player.js,Song.js)。
(4)pecRunner.html:運行測試用例的環境。它將上面3個文件夾中一些必要的文件都包含了進來。如果你想將自己的測試添加進來的話,那么就修改相應的路徑。
2.核心概念
(1)細則套件Suites
Suite表示一個測試集,以函數describe(string, function)封裝。describe函數是Jasmine套件定義的一個全局函數,所以可以在測試中直接調用。
describe()函數帶有兩個參數,一個字符串,一個函數。字符串是待建立的細則(spec)套件名稱或者描述,函數封裝了測試套件。
describe()函數帶有兩個參數,一個字符串,一個函數。字符串是待建立的細則(spec)套件名稱或者描述,函數封裝了測試套件。
可以嵌套這些describe()函數,這樣我們可以創建一個測試樹來執行那些在測試中設置的不同條件。如:
describe('Unit test: MainController', function() {
describe('index method', function() {
// 細則放這里
});
});
describe('Unit test: MainController', function() {
describe('index method', function() {
// 細則放這里
});
});
一個Suite(describe)包含多個Specs(it),一個Specs(it)包含多個斷言(expect)。
使用describe()函數把相關的細則分組是個不錯的主意。在每個describe()塊運行時,這些字符串會沿着細則的名稱鏈接起來。因此,上面這個例子的標題就會變成“Unit test:MainController index method.”然后,這些describe()塊的標題就會被追加到細則的標題上。設計這個步驟的目的是讓我們以完整句子來閱讀細則的,所以把測試命名成可讀的英文就很重要了。
(2)定義一個細則
Spec表示測試用例,以it(string, function)函數封裝。我們通過調用it()函數來定義一個細則。這個函數也是在Jasmine測試套件中定義的全局函數,所以可以從測試中直接調用。
it()函數帶有兩個參數:一個字符串,是細則的標題或者描述;一個函數,包含了一個或多個用於測試代碼功能的預期。
這些預期都是函數,執行時評估為true或false。一個所有預期都為true的測試就算是一條通過的細則,一條細則有一個或者多個預期為false的話,就是個失敗的測試。
一個簡單的測試可能像這樣:
describe('A spec suite', function() {
it('contains a passing spec', function() {
expect(true).toBe(true);
});
});
這個細則的標題,追加到describe()標題之后,就成為了“一個細則套件包含一條已通過的細則”。
(3)預期
describe('A spec suite', function() {
it('contains a passing spec', function() {
expect(true).toBe(true);
});
});
這個細則的標題,追加到describe()標題之后,就成為了“一個細則套件包含一條已通過的細則”。
(3)預期
測試應用時,我們會想要斷言條件在應用的不同階段是符合我們期望的。我們要寫的這個測試讀起來就像這樣:“如果我們點擊這個按鈕,就期望有這個結果。”例如,“如果我們導航到首頁,我們期望歡迎信息會被渲染出來。”
使用expect()函數來建立預期。expect()函數帶有一個單值參數。這個參數被稱為真實值。
要建立一個預期,我們給它串聯一個帶單值參數的匹配器函數,這個參數就是期望值。
這些匹配器函數實現了一個在真實值和期望值之間的布爾比較。可以通過在調用匹配器之前調一個not來創建測試的否定式。
這些匹配器函數實現了一個在真實值和期望值之間的布爾比較。可以通過在調用匹配器之前調一個not來創建測試的否定式。
(4)內置匹配器matchers
常用的Matchers有:
toBe():相當於===比較。
toNotBe()
toBeDefined():檢查變量或屬性是否已聲明且賦值。
toBeUndefined()
toBeNull():是否是null。
toBeTruthy():如果轉換為布爾值,是否為true。
toBeFalsy()
toBeLessThan():數值比較,小於。
toBeGreaterThan():數值比較,大於。
toBe():相當於===比較。
toNotBe()
toBeDefined():檢查變量或屬性是否已聲明且賦值。
toBeUndefined()
toBeNull():是否是null。
toBeTruthy():如果轉換為布爾值,是否為true。
toBeFalsy()
toBeLessThan():數值比較,小於。
toBeGreaterThan():數值比較,大於。
toEqual():相當於==,注意與toBe()的區別。
toNotEqual()
toContain():數組中是否包含元素(值)。只能用於數組,不能用於對象。
toBeCloseTo():數值比較時定義精度,先四舍五入后再比較。
toContain():數組中是否包含元素(值)。只能用於數組,不能用於對象。
toBeCloseTo():數值比較時定義精度,先四舍五入后再比較。
toHaveBeenCalled()
toHaveBeenCalledWith()
toMatch():按正則表達式匹配。
toNotMatch()
toHaveBeenCalledWith()
toMatch():按正則表達式匹配。
toNotMatch()
toThrow():檢驗一個函數是否會拋出一個錯誤
所有的matchers匹配器支持添加 .not反轉結果: expect(x).not.toEqual(y);
舉例:
describe('A spec suite', function() {
it('contains a passing spec', function() {
var value = "<h2>Header element: welcome</h2>";
expect(value).toMatch(/welcome/);
expect(value).toMatch('welcome');
expect(value).not.toMatch('goodbye');
});
});
it('contains a passing spec', function() {
var value = "<h2>Header element: welcome</h2>";
expect(value).toMatch(/welcome/);
expect(value).toMatch('welcome');
expect(value).not.toMatch('goodbye');
});
});
describe('A spec suite', function() {
it('contains a passing spec', function() {
var arr = [1,2,3,4];
expect(arr).toContain(4);
expect(arr).not.toContain(12);
});
});
it('contains a passing spec', function() {
var arr = [1,2,3,4];
expect(arr).toContain(4);
expect(arr).not.toContain(12);
});
});
describe('A spec suite', function() {
it('contains a passing spec', function() {
expect(function() {
return a + 10;
}).toThrow();
expect(function() {
return 2 + 10;
}).not.toThrow();
});
});
it('contains a passing spec', function() {
expect(function() {
return a + 10;
}).toThrow();
expect(function() {
return 2 + 10;
}).not.toThrow();
});
});
(5)自定義matcher匹配器
自定義Matcher(被稱為Matcher Factories)實質上是一個函數(該函數的參數可以為空),該函數返回一個閉包,該閉包的本質是一個compare函數,compare函數接受2個參數:actual value 和 expected value。
compare函數必須返回一個帶pass屬性的結果Object,pass屬性是一個Boolean值,表示該Matcher的結果(為true表示該Matcher實際值與預期值匹配,為false表示不匹配),也就是說,實際值與預期值具體的比較操作的結果,存放於pass屬性中。
(6)Setup和Teardown操作
Jasmine的Setup和Teardown操作(Setup在每個測試用例Spec執行之前做一些初始化操作,Teardown在每個Sepc執行完之后做一些清理操作,這兩個函數名稱來自於JUnit),是由一組全局beforeEach,afterEach, beforeAll,afterAll函數來實現的。
beforeEach():在describe函數中每個Spec執行之前執行。
afterEach(): 在describe函數中每個Spec數執行之后執行。
beforeAll():在describe函數中所有的Specs執行之前執行,但只執行一次,在Sepc之間並不會被執行。
afterAll(): 在describe函數中所有的Specs執行之后執行,但只執行一次,在Sepc之間並不會被執行。
beforeAll 和 afterAll適用於執行比較耗時或者耗資源的一些共同的初始化和清理工作。而且在使用時還要注意,它們不會在每個Spec之間執行,所以不適用於每次執行前都需要干凈環境的Spec。
(7)this關鍵字
除了在describe函數開始定義變量,用於各it函數共享數據外,還可以通過this關鍵字來共享數據。
在每一個Spec的生命周期(beforeEach->it->afterEach)的開始,都將有一個空的this對象(在開始下一個Spec周期時,this會被重置為空對象)。
3.高級特性
1.Spy
Spy能監測任何function的調用和方法參數的調用痕跡。需使用2個特殊的Matcher:
toHaveBeenCalled:可以檢查function是否被調用過,
toHaveBeenCalledWith: 可以檢查傳入參數是否被作為參數調用過。
2.spyOn
使用spyOn(obj,'function')來為obj的function方法聲明一個Spy。不過要注意的一點是,對Spy函數的調用並不會影響真實的值。
describe("A spy", function() {
var foo, bar = null;
beforeEach(function() {
foo = {
setBar: function(value) {
bar = value;
}
};
spyOn(foo, 'setBar');
foo.setBar(123);
foo.setBar(456, 'another param');
});
it("tracks that the spy was called", function() {
expect(foo.setBar).toHaveBeenCalled();
});
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() {
// Spy的調用並不會影響真實的值,所以bar仍然是null。
expect(bar).toBeNull();
});
});
3.and.callThrough
如果在spyOn之后鏈式調用and.callThrough,那么Spy除了跟蹤所有的函數調用外,還會直接調用函數的真實實現,因此Spy返回的值就是函數調用后實際的值了。
...
spyOn(foo, 'getBar').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(bar).toEqual(123);
});
it("when called returns the requested value", function() {
expect(fetchedBar).toEqual(123);
});
});
4.全局匹配謂詞
(1)jasmine.any
jasmine.any的參數為一個構造函數,用於檢測該參數是否與實際值所對應的構造函數相匹配。
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));
});
});
});
(2)
jasmine.anything
jasmine.anything
用於檢測實際值是否為
null
或
undefined
,如果不為
null
或
undefined
,則返回
true
。
it("matches anything", function() {
expect(1).toEqual(jasmine.anything());});
(3)jasmine.objectContaining
用於檢測實際Object值中是否存在特定key/value對。
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
}));
});
5.Jasmine Clock
Jasmine Clock用於setTimeout和setInterval的回調控制,它使timer的回調函數同步化,不再依賴於具體的時間,而是將時間離散化,使測試人員能精確控制具體的時間點。
調用jasmine.clock().install()可以在特定的需要操縱時間的Spec或者Suite中安裝Jasmine Clock,注意操作完后要調用jasmine.clock().uninstall()進行卸載。
var timerCallback;
beforeEach(function() {
timerCallback = jasmine.createSpy("timerCallback");
jasmine.clock().install();
});
afterEach(function() {
jasmine.clock().uninstall();
});
6.模擬超時(Mocking Timeout)
可以調用jasmine.clock().tick(nTime)來模擬計時,一旦tick中設置的時間nTime,其累計設置的值達到setTimeout或setInterval中指定的延時時間,則觸發回調函數。
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);
//tick設置的時間,累計到此201ms,因此會觸發setInterval中的毀掉函數被調用2次。
jasmine.clock().tick(50);
expect(timerCallback.calls.count()).toEqual(2);
});
三 模擬
1.什么是mock
在開始寫測試之前,我們需要理解測試的一個核心特性:模擬。模擬允許我們在受控環境下定義模擬對象來模仿真實對象的行為。AngularJS提供了自己的模擬庫:angular-mocks,位於angular-mock.js文件中,因此如果要在單元測試中建立模擬對象,就必須確保在Karma配置中,即test/karma.conf.js文件的file數組中包含了angular-mock.js。
2.ng-mock中的一些常用的方法
(1)angular.mock.module
此方法非常方便調用,因為angular.mock.module函數被發布在全局作用域的window接口上了。
module是用來配置inject方法注入的模塊信息,參數可以是字符串,函數,對象,它一般用在beforeEach方法里,因為這個可以確保在執行測試任務的時候,inject方法可以獲取到模塊配置。
describe('myApp',function(){
//模擬'myApp'angular模塊
beforeEach(angular.mock.module('myApp'));
it('....')
});
建立了模擬的angular模塊之后,可以把連接到這個模塊上的任意服務注入到測試代碼中。在我們的測試代碼中,注入依賴關系很重要,因為我們隔離了想要測試的功能。
(2)angular.mock.inject
inject函數也是在window對象上的,為的是全局訪問,因此可以直接調用inject。
inject是用來注入上面配置好的ng模塊,方便在it的測試函數里調用。
describe('myApp',function(){
var scope;
beforeEach(angular.mock.module('myApp'));
beforeEach(angular.mock.inject(function($rootscope){
scope=$rootscope.$new();
});
it('...');
});
通常我們會用將引入進測試時使用的名字來保存它。比如說,如果我們在測試一個服務,可以注入這個服務,然后把它的引用用一種稍微不同的命名方案存儲起來。在注入的服務名稱兩端使用下划線,當它被注入時,注入器會忽略它的名稱。
describe('myApp',function(){
var scope;
beforeEach(angular.mock.module('myApp'));
beforeEach(angular.mock.inject(function(_myService_){
myService=_myService_;
});
it('...');
});
3.模擬$httpBackend
angualr內置了$httpBackend模擬庫,這樣我們可以在應用中模擬任何外部的XHR請求,避免在測試中創建昂貴的$http請求。
如:
var app = angular.module('Application', []);
app.controller('MainCtrl', function($scope, $http) {
$http.get('Users/users.json').success(function(data){
$scope.users = data;
});
$scope.text = 'Hello World!';
});
測試:
describe('MainCtrl', function() {
//我們會在測試中使用這個scope
var scope, $httpBackend;
//模擬我們的Application模塊並注入我們自己的依賴
beforeEach(angular.mock.module('Application'));
//模擬Controller,並且包含 $rootScope 和 $controller
beforeEach(angular.mock.inject(function($rootScope, $controller, _$httpBackend_) {
//設置$httpBackend沖刷$http請求
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', 'Users/users.json').respond([{
id: 1,
name: 'Bob'
}, {
id: 2,
name: 'Jane'
}]);
//創建一個空的 scope
scope = $rootScope.$new();
//聲明 Controller並且注入已創建的空的 scope
$controller('MainCtrl', {
$scope: scope
});
}));
// 測試從這里開始
it('should have variable text = "Hello World!"', function() {
expect(scope.text).toBe('Hello World!');
});
it('should fetch list of users', function() {
$httpBackend.flush();
expect(scope.users.length).toBe(2);
expect(scope.users[0].name).toBe('Bob');
//輸出結果以方便查看
for(var i=0;i<scope.users.length;i++){
console.log(scope.users[i].name);
}
});
});
可以使用$httpBackend.when和$httpBackend.expect提前設置請求的偽數據,最后在請求后執行$httpBackend.flush就會立即執行完成http請求。
4.$httpBackend常用方法
(1)when :新建一個后端定義(backend definition)。
when(method, url, [data], [headers]);
(2)expect :新建一個請求期望(request expectation)。
expect(method, url, [data], [headers]);
method表示http方法注意都需要是大寫(GET, PUT…);
url請求的url可以為正則或者字符串;
data請求時帶的參數,
headers請求時設置的header。
如果這些參數都提供了,那只有當這些參數都匹配的時候才會正確的匹配請求。when和expect都會返回一個帶respond方法的對象。respond方法有3個參數status,data,headers通過設置這3個參數就可以偽造返回的響應數據了。
$httpBackend.when與$httpBackend.expect的區別在於:$httpBackend.expect的偽后台只能被調用一次(調用一次后會被清除),第二次調用就會報錯,而且$httpBackend.resetExpectations可以移除所有的expect而對when沒有影響。
快捷方法:when和expect都有對應的快捷方法whenGET, whenPOST,whenHEAD, whenJSONP, whenDELETE, whenPUT; expect也一樣。
使用快捷方法進行測試:
$httpBackend.whenGET('/someUrl').respond({name:'wolf'},{'X-Record-Count':100}); //聲明Mock服務,模擬后端服務器行為
//調用網絡接口
$http.get('/someUrl').success(function(data){
expect(data.name).toBe('wolf');
});
//刷新一次,模擬后端返回請求,在調用這個命令之前,success中的回調函數不會被執行
$httpBackend.flush();