Angular.Js的自動(dòng)化測(cè)試詳解
本文著重介紹關(guān)于ng的測(cè)試部分,主要包括以下三個(gè)方面:
- 框架的選擇(Karma+Jasmine)
- 測(cè)試的分類和選擇(單元測(cè)試 + 端到端測(cè)試)
- 在ng中各個(gè)模塊如何編寫測(cè)試用例
下面各部分進(jìn)行詳細(xì)介紹。
測(cè)試的分類
在測(cè)試中,一般分為單元測(cè)試和端到端測(cè)試,單元測(cè)試是保證開發(fā)者驗(yàn)證代碼某部分有效性的技術(shù),端到端(E2E)是當(dāng)你想確保一堆組件能按事先預(yù)想的方式運(yùn)行起來的時(shí)候使用。
其中單元測(cè)試又分為兩類: TDD(測(cè)試驅(qū)動(dòng)開發(fā))和BDD(行為驅(qū)動(dòng)開發(fā))。
下面著重介紹兩種開發(fā)模式。
TDD(測(cè)試驅(qū)動(dòng)開發(fā) Test-driven development)是使用測(cè)試案例等來驅(qū)動(dòng)你的軟件開發(fā)。
如果我們想要更深入點(diǎn)了解TDD,我們可以將它分成五個(gè)不同的階段:
- 首先,開發(fā)人員編寫一些測(cè)試方法。
- 其次,開發(fā)人員使用這些測(cè)試,但是很明顯的,測(cè)試都沒有通過,原因是還沒有編寫這些功能的代碼來實(shí)際執(zhí)行。
- 接下來,開發(fā)人員實(shí)現(xiàn)測(cè)試中的代碼。
- 如果開發(fā)人員寫代碼很優(yōu)秀,那么在下一階段會(huì)看到他的測(cè)試通過。
- 然后開發(fā)人員可以重構(gòu)自己的代碼,添加注釋,使其變得整潔,開發(fā)人員知道,如果新添加的代碼破壞了什么,那么測(cè)試會(huì)提醒他失敗。
其中的流程圖如下:

TDD
TDD的好處:
- 能驅(qū)使系統(tǒng)最終的實(shí)現(xiàn)代碼,都可以被測(cè)試代碼所覆蓋到,也即“每一行代碼都可測(cè)”。
- 測(cè)試代碼作為實(shí)現(xiàn)代碼的正確導(dǎo)向,最終演變?yōu)檎_系統(tǒng)的行為,能讓整個(gè)開發(fā)過程更加高效。
BDD是(行為驅(qū)動(dòng)開發(fā) Behavior-Driven Development)指的是不應(yīng)該針對(duì)代碼的實(shí)現(xiàn)細(xì)節(jié)寫測(cè)試,而是要針對(duì)行為寫測(cè)試。BDD測(cè)試的是行為,即軟件應(yīng)該怎樣運(yùn)行。
- 和TDD比起來,BDD是需要我們先寫行為規(guī)范(功能明細(xì)),在進(jìn)行軟件開發(fā)。功能明細(xì)和測(cè)試看起來非常相似,但是功能明細(xì)更加含蓄一些。BDD采用了更詳細(xì)的方式使得它看起來就像是一句話。
- BDD測(cè)試應(yīng)該注重功能而不是實(shí)際的結(jié)果。你常常會(huì)聽說BDD是幫助設(shè)計(jì)軟件,而不是像TDD那樣的測(cè)試軟件。
最后總結(jié):TDD的迭代反復(fù)驗(yàn)證是敏捷開發(fā)的保障,但沒有明確如何根據(jù)設(shè)計(jì)產(chǎn)生測(cè)試,并保障測(cè)試用例的質(zhì)量,而BDD倡導(dǎo)大家都用簡(jiǎn)潔的自然語言描述系統(tǒng)行為的理念,恰好彌補(bǔ)了測(cè)試用例(即系統(tǒng)行為)的準(zhǔn)確性。
測(cè)試框架選擇
利用karma和jasmine來進(jìn)行ng模塊的單元測(cè)試。
Karma:是一個(gè)基于Node.js的JavaScript測(cè)試執(zhí)行過程管理工具,這個(gè)測(cè)試工具的一個(gè)強(qiáng)大特性就是,它可以監(jiān)控(Watch)文件的變化,然后自行執(zhí)行,通過console.log顯示測(cè)試結(jié)果。
jasmine是一個(gè)行為驅(qū)動(dòng)開發(fā)(BDD)的測(cè)試框架,不依賴任何js框架以及dom,是一個(gè)非常干凈以及友好API的測(cè)試庫.
Karma
karma是一個(gè)單元測(cè)試的運(yùn)行控制框架,提供以不同環(huán)境來運(yùn)行單元測(cè)試,比如chrome,firfox,phantomjs等,測(cè)試框架支持jasmine,mocha,qunit,是一個(gè)以nodejs為環(huán)境的npm模塊.
Karma從頭開始構(gòu)建,免去了設(shè)置測(cè)試的負(fù)擔(dān),集中精力在應(yīng)用邏輯上。會(huì)產(chǎn)生一個(gè)瀏覽器實(shí)例,針對(duì)不同瀏覽器運(yùn)行測(cè)試,同時(shí)可以對(duì)測(cè)試的運(yùn)行進(jìn)行一個(gè)實(shí)時(shí)反饋,提供一份debug報(bào)告。
測(cè)試還會(huì)依賴一些Karma插件,如測(cè)試覆蓋率Karma-coverage工具、Karman-fixture工具及Karma-coffee處理工具。此外,前端社區(qū)里提供里比較豐富的插件,常見的測(cè)試需求都能涵蓋到。
安裝測(cè)試相關(guān)的npm模塊建議使用—-save-dev參數(shù),因?yàn)檫@是開發(fā)相關(guān)的,一般的運(yùn)行karma的話只需要下面兩個(gè)npm命令:
npm install karma --save-dev npm install karma-junit-reporter --save-dev
然后一個(gè)典型的運(yùn)行框架通常都需要一個(gè)配置文件,在karma里可以是一個(gè)karma.conf.js,里面的代碼是一個(gè)nodejs風(fēng)格的,一個(gè)普通的例子如下:
module.exports = function(config){
config.set({
// 下面files里的基礎(chǔ)目錄
basePath : '../',
// 測(cè)試環(huán)境需要加載的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'
],
// 是否自動(dòng)監(jiān)聽上面文件的改變自動(dòng)運(yùn)行測(cè)試
autoWatch : true,
// 應(yīng)用的測(cè)試框架
frameworks: ['jasmine'],
// 用什么環(huán)境測(cè)試代碼,這里是chrome`
browsers : ['Chrome'],
// 用到的插件,比如chrome瀏覽器與jasmine插件
plugins : [
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-jasmine',
'karma-junit-reporter'
],
// 測(cè)試內(nèi)容的輸出以及導(dǎo)出用的模塊名
reporters: ['progress', 'junit'],
// 設(shè)置輸出測(cè)試內(nèi)容文件的信息
junitReporter : {
outputFile: 'test_out/unit.xml',
suite: 'unit'
}
});
};
運(yùn)行時(shí)輸入:
karma start test/karma.conf.js
jasmine
jasmine是一個(gè)行為驅(qū)動(dòng)開發(fā)的測(cè)試框架,不依賴任何js框架以及dom,是一個(gè)非常干凈以及友好API的測(cè)試庫.
以下以一個(gè)具體實(shí)例說明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);
});
});
- 首先任何一個(gè)測(cè)試用例以describe函數(shù)來定義,它有兩參數(shù),第一個(gè)用來描述測(cè)試大體的中心內(nèi)容,第二個(gè)參數(shù)是一個(gè)函數(shù),里面寫一些真實(shí)的測(cè)試代碼
- it是用來定義單個(gè)具體測(cè)試任務(wù),也有兩個(gè)參數(shù),第一個(gè)用來描述測(cè)試內(nèi)容,第二個(gè)參數(shù)是一個(gè)函數(shù),里面存放一些測(cè)試方法
- expect主要用來計(jì)算一個(gè)變量或者一個(gè)表達(dá)式的值,然后用來跟期望的值比較或者做一些其它的事件
- beforeEach與afterEach主要是用來在執(zhí)行測(cè)試任務(wù)之前和之后做一些事情,上面的例子就是在執(zhí)行之前改變變量的值,然后在執(zhí)行完成之后重置變量的值
開始單元測(cè)試
下面分別以控制器,指令,過濾器和服務(wù)四個(gè)部分來編寫相關(guān)的單元測(cè)試。項(xiàng)目地址為angular-seed(點(diǎn)我)項(xiàng)目,可以下載demo并運(yùn)行其測(cè)試用例。
demo中是一個(gè)簡(jiǎn)單的todo應(yīng)用,會(huì)包含一個(gè)文本輸入框,其中可以編寫一些筆記,按下按鈕可以將新的筆記加入筆記列表中,其中使用notesfactory封裝LocalStorage來儲(chǔ)存筆記信息。
先介紹一下angular中測(cè)試相關(guān)的組件angular-mocks。
了解angular-mocks
在Angular中,模塊都是通過依賴注入來加載和實(shí)例化的,因此官方提供了angular-mocks.js測(cè)試工具來提供模塊的定義、加載,依賴注入等功能。
其中一些常用的方法(掛載在window命名空間下):
angular.mock.module: module用來加載已有的模塊,以及配置inject方法注入的模塊信息。具體使用如下:
beforeEach(module('myApp.filters'));
beforeEach(module(function($provide) {
$provide.value('version', 'TEST_VER');
}));
該方法一般在beforeEach中使用,在執(zhí)行測(cè)試用例之前可以獲得模塊的配置。
angular.mock.inject: inject用來注入配置好的ng模塊,來供測(cè)試用例里進(jìn)行調(diào)用。具體使用如下:
it('should provide a version', inject(function(mode, version) {
expect(version).toEqual('v1.0.1');
expect(mode).toEqual('app');
}));
其實(shí)inject里面就是利用angular.inject方法創(chuàng)建的一個(gè)內(nèi)置的依賴注入實(shí)例,然后里面的模塊和普通的ng模塊的依賴處理是一樣的。
Controller部分
Angular模塊是todoApp,控制器是TodoController,當(dāng)按鈕被點(diǎn)擊時(shí),TodoController的createNote()函數(shù)會(huì)被調(diào)用。下面是app.js的代碼部分。
var todoApp = angular.module('todoApp',[]);
todoApp.controller('TodoController',function($scope,notesFactory){
$scope.notes = notesFactory.get();
$scope.createNote = function(){
notesFactory.put($scope.note);
$scope.note='';
$scope.notes = notesFactory.get();
}
});
todoApp.factory('notesFactory',function(){
return {
put: function(note){
localStorage.setItem('todo' + (Object.keys(localStorage).length + 1), note);
},
get: function(){
var notes = [];
var keys = Object.keys(localStorage);
for(var i = 0; i < keys.length; i++){
notes.push(localStorage.getItem(keys[i]));
}
return notes;
}
};
});
在todoController中用了個(gè)叫做notesFactory的服務(wù)來存儲(chǔ)和提取筆記。當(dāng)createNote()被調(diào)用時(shí),會(huì)使用這個(gè)服務(wù)將一條信息存入LocalStorage中,然后清空當(dāng)前的note。因此,在編寫測(cè)試模塊是,應(yīng)該保證控制器初始化,scope中有一定數(shù)量的筆記,在調(diào)用createNote()之后,筆記的數(shù)量應(yīng)該加一。
具體的單元測(cè)試如下:
describe('TodoController Test', function() {
beforeEach(module('todoApp')); // 將會(huì)在所有的it()之前運(yùn)行
// 我們?cè)谶@里不需要真正的factory。因此我們使用一個(gè)假的factory。
var mockService = {
notes: ['note1', 'note2'], //僅僅初始化兩個(gè)項(xiàng)目
get: function() {
return this.notes;
},
put: function(content) {
this.notes.push(content);
}
};
// 現(xiàn)在是真正的東西,測(cè)試spec
it('should return notes array with two elements initially and then add one',
inject(function($rootScope, $controller) { //注入依賴項(xiàng)目
var scope = $rootScope.$new();
// 在創(chuàng)建控制器的時(shí)候,我們也要注入依賴項(xiàng)目
var ctrl = $controller('TodoController', {$scope: scope, notesFactory:mockService});
// 初始化的技術(shù)應(yīng)該是2
expect(scope.notes.length).toBe(2);
// 輸入一個(gè)新項(xiàng)目
scope.note = 'test3';
// now run the function that adds a new note (the result of hitting the button in HTML)
// 現(xiàn)在運(yùn)行這個(gè)函數(shù),它將會(huì)增加一個(gè)新的筆記項(xiàng)目
scope.createNote();
// 期待現(xiàn)在的筆記數(shù)目是3
expect(scope.notes.length).toBe(3);
})
);
});
在beforeEach中,每一個(gè)測(cè)試用例被執(zhí)行之前,都需要加載模塊module("todoApp") 。
由于不需要外部以來,因此我們本地建立一個(gè)假的mockService來代替factory,用來模擬noteFactory,其中包含相同的函數(shù),get()和put() 。這個(gè)假的factory從數(shù)組中加載數(shù)據(jù)代替localStorage的操作。
在it中,聲明了依賴項(xiàng)目$rootScope和$controller,都可以由Angular自動(dòng)注入,其中$rootScope用來獲得根作用域,$controller用作創(chuàng)建新的控制器。
$controller服務(wù)需要兩個(gè)參數(shù)。第一個(gè)參數(shù)是將要?jiǎng)?chuàng)建的控制器的名稱。第二個(gè)參數(shù)是一個(gè)代表控制器依賴項(xiàng)目的對(duì)象,
$rootScope.$new()方法將會(huì)返回一個(gè)新的作用域,它用來注入控制器。同時(shí)我們傳入mockService作為假factory。
之后,初始化會(huì)根據(jù)notes數(shù)組的長(zhǎng)度預(yù)測(cè)筆記的數(shù)量,同時(shí)在執(zhí)行了createNote()函數(shù)之后,會(huì)改變數(shù)組的長(zhǎng)度,因此可以寫出兩個(gè)測(cè)試用例。
Factory部分
factory部分的單元測(cè)試代碼如下:
describe('notesFactory tests', function() {
var factory;
// 在所有it()函數(shù)之前運(yùn)行
beforeEach(function() {
// 載入模塊
module('todoApp');
// 注入你的factory服務(wù)
inject(function(notesFactory) {
factory = notesFactory;
});
var store = {
todo1: 'test1',
todo2: 'test2',
todo3: 'test3'
};
spyOn(localStorage, 'getItem').andCallFake(function(key) {
return store[key];
});
spyOn(localStorage, 'setItem').andCallFake(function(key, value) {
return store[key] = value + '';
});
spyOn(localStorage, 'clear').andCallFake(function() {
store = {};
});
spyOn(Object, 'keys').andCallFake(function(value) {
var keys=[];
for(var key in store) {
keys.push(key);
}
return keys;
});
});
// 檢查是否有我們想要的函數(shù)
it('should have a get function', function() {
expect(angular.isFunction(factory.get)).toBe(true);
expect(angular.isFunction(factory.put)).toBe(true);
});
// 檢查是否返回3條記錄
it('should return three todo notes initially', function() {
var result = factory.get();
expect(result.length).toBe(3);
});
// 檢查是否添加了一條新紀(jì)錄
it('should return four todo notes after adding one more', function() {
factory.put('Angular is awesome');
var result = factory.get();
expect(result.length).toBe(4);
});
});
在TodoController模塊中,實(shí)際上的factory會(huì)調(diào)用localStorage來存儲(chǔ)和提取筆記的項(xiàng)目,但由于我們單元測(cè)試中,不需要依賴外部服務(wù)去獲取和存儲(chǔ)數(shù)據(jù),因此我們要對(duì)localStorage.getItem()和localStorage.setItem()進(jìn)行spy操作,也就是利用假函數(shù)來代替這兩個(gè)部分。
spyOn(localStorage,'setItem')andCallFake()是用來用假函數(shù)進(jìn)行監(jiān)聽的。第一個(gè)參數(shù)指定需要監(jiān)聽的對(duì)象,第二個(gè)參數(shù)指定需要監(jiān)聽的函數(shù),然后andCallfake這個(gè)API可以編寫自己的函數(shù)。因此,測(cè)試中完成了對(duì)localStorage和Object的改寫,使函數(shù)可以返回我們自己數(shù)組中的值。
在測(cè)試用例中,首先檢測(cè)新封裝的factory函數(shù)是否包含了get()和put()這兩個(gè)方法,,然后進(jìn)行factory.put()操作后斷言筆記的數(shù)量。
Filter部分
我們添加一個(gè)過濾器。truncate的作用是如果傳入字符串過長(zhǎng)后截取前10位。源碼如下:
todoApp.filter('truncate',function(){
return function(input,length){
return (input.length > length ? input.substring(0,length) : input);
}
});
所以在單元測(cè)試中,可以根據(jù)傳入字符串的情況斷言生成子串的長(zhǎng)度。
describe('filter test',function(){
beforeEach(module('todoApp'));
it('should truncate the input to 1o characters',inject(function(truncateFilter){
expect(truncateFilter('abcdefghijkl',10).length).toBe(10);
});
);
});
之前已經(jīng)對(duì)斷言進(jìn)行討論了,值得注意的一點(diǎn)是我們需要在調(diào)用過濾器的時(shí)候在名稱后面加入Filter,然后正常調(diào)用即可。
Directive部分
源碼中的指令部分:
todoApp.directive('customColor', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
elem.css({'background-color': attrs.customColor});
}
};
});
由于指令必須編譯之后才能生成相關(guān)的模板,因此我們要引入$compile服務(wù)來完成實(shí)際的編譯,然后再測(cè)試我們想要進(jìn)行測(cè)試的元素。
angular.element()會(huì)創(chuàng)建一個(gè)jqLite元素,然后我們將其編譯到一個(gè)新生成的自作用域中,就可以被測(cè)試了。具體測(cè)試用例如下:
describe('directive tests',function(){
beforeEach(module('todoApp'));
it('should set background to rgb(128, 128, 128)',
inject(function($compile,$rootScope) {
scope = $rootScope.$new();
// 獲得一個(gè)元素
elem = angular.element("<span custom-color=\"rgb(128, 128, 128)\">sample</span>");
// 創(chuàng)建一個(gè)新的自作用域
scope = $rootScope.$new();
// 最后編譯HTML
$compile(elem)(scope);
// 希望元素的背景色和我們所想的一樣
expect(elem.css("background-color")).toEqual('rgb(128, 128, 128)');
})
);
});
開始端到端測(cè)試
在端到端測(cè)試中,我們需要從用戶的角度出發(fā),來進(jìn)行黑盒測(cè)試,因此會(huì)涉及到一些DOM操作。將一對(duì)組件組合起來然后檢查是否如預(yù)想的結(jié)果一樣。
在這個(gè)demo中,我們模擬用戶輸入信息并按下按鈕的過程,檢測(cè)信息能否被添加到localStorage中。
在E2E測(cè)試中,需要引入angular-scenario這個(gè)文件,并且建立一個(gè)html作為運(yùn)行report的展示,在html中包含帶有e2e測(cè)試代碼的執(zhí)行js文件,在編寫完測(cè)試之后,運(yùn)行該html文件查看結(jié)果。具體的e2e代碼如下:
describe('my app', function() {
beforeEach(function() {
browser().navigateTo('../../app/notes.html');
});
var oldCount = -1;
it("entering note and performing click", function() {
element('ul').query(function($el, done) {
oldCount = $el.children().length;
done();
});
input('note').enter('test data');
element('button').query(function($el, done) {
$el.click();
done();
});
});
it('should add one more element now', function() {
expect(repeater('ul li').count()).toBe(oldCount + 1);
});
});
我們?cè)诙说蕉藴y(cè)試過程中,首先導(dǎo)航到我們的主html頁面app/notes.html,可以通過browser.navigateTo()來完成,element.query()函數(shù)選擇了ul元素并記錄其中有多少個(gè)初始化的項(xiàng)目,存放在oldCount變量中。
然后通過input('note').enter()來鍵入一個(gè)新的筆記,然后模擬一下點(diǎn)擊操作來檢查是否增加了一個(gè)新的筆記(li元素)。然后通過斷言可以將新舊的筆記數(shù)進(jìn)行對(duì)比。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流。
- JavaScript 實(shí)現(xiàn)自己的安卓手機(jī)自動(dòng)化工具腳本(推薦)
- JavaScript 常見安全漏洞和自動(dòng)化檢測(cè)技術(shù)
- 使用auto.js實(shí)現(xiàn)自動(dòng)化每日打卡功能
- PyQt5內(nèi)嵌瀏覽器注入JavaScript腳本實(shí)現(xiàn)自動(dòng)化操作的代碼實(shí)例
- Angular.js項(xiàng)目中使用gulp實(shí)現(xiàn)自動(dòng)化構(gòu)建以及壓縮打包詳解
- nodejs前端自動(dòng)化構(gòu)建環(huán)境的搭建
- 從零搭建docker+jenkins+node.js自動(dòng)化部署環(huán)境的方法
- Angular.js自動(dòng)化測(cè)試之protractor詳解
- python接口自動(dòng)化(十七)--Json 數(shù)據(jù)處理---一次爬坑記(詳解)
- JavaScript揭秘:實(shí)現(xiàn)自動(dòng)化連連看游戲
相關(guān)文章
基于angular-utils-ui-breadcrumbs使用心得(分享)
下面小編就為大家?guī)硪黄赼ngular-utils-ui-breadcrumbs使用心得(分享)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-11-11
angular 實(shí)時(shí)監(jiān)聽input框value值的變化觸發(fā)函數(shù)方法
今天小編就為大家分享一篇angular 實(shí)時(shí)監(jiān)聽input框value值的變化觸發(fā)函數(shù)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-08-08
詳解angularJs模塊ui-router之狀態(tài)嵌套和視圖嵌套
這篇文章主要介紹了詳解angularJs模塊ui-router之狀態(tài)嵌套和視圖嵌套,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04
AngularJS的依賴注入實(shí)例分析(使用module和injector)
這篇文章主要介紹了AngularJS的依賴注入,結(jié)合實(shí)例形式分析了依賴注入的原理及使用module和injector實(shí)現(xiàn)依賴注入的步驟與操作技巧,需要的朋友可以參考下2017-01-01
詳解AngularJS1.x學(xué)習(xí)directive 中‘& ’‘=’ ‘@’符號(hào)的區(qū)別使用
這篇文章主要介紹了詳解AngularJS1.x學(xué)習(xí)directive 中‘& ’‘=’ ‘@’符號(hào)的區(qū)別使用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-08-08

