Angular.Js的自動化測試詳解_AngularJS

來源:互聯網
上載者:User

本文著重介紹關於ng的測試部分,主要包括以下三個方面:

  1. 架構的選擇(Karma+Jasmine)
  2. 測試的分類和選擇(單元測試 + 端到端測試)
  3. 在ng中各個模組如何編寫測試案例

下面各部分進行詳細介紹。

測試的分類

在測試中,一般分為單元測試和端到端測試,單元測試是保證開發人員驗證代碼某部分有效性的技術,端到端(E2E)是當你想確保一堆組件能按事先預想的方式運行起來的時候使用。

其中單元測試又分為兩類: TDD(測試驅動開發)和BDD(行為驅動開發)。

下面著重介紹兩種開發模式。

TDD(測試驅動開發 Test-driven development)是使用測試案例等來驅動你的軟體開發。

如果我們想要更深入點瞭解TDD,我們可以將它分成五個不同的階段:

  1.      首先,開發人員編寫一些測試方法。
  2.      其次,開發人員使用這些測試,但是很明顯的,測試都沒有通過,原因是還沒有編寫這些功能的代碼來實際執行。
  3.      接下來,開發人員實現測試中的代碼。
  4.      如果開發人員寫代碼很優秀,那麼在下一階段會看到他的測試通過。
  5.      然後開發人員可以重構自己的代碼,添加註釋,使其變得整潔,開發人員知道,如果新添加的代碼破壞了什麼,那麼測試會提醒他失敗。

其中的流程圖如下:


TDD

TDD的好處:

  1.      能驅使系統最終的實現代碼,都可以被測試代碼所覆蓋到,也即“每一行代碼都可測”。
  2.      測試代碼作為實現代碼的正確導向,最終演變為正確系統的行為,能讓整個開發過程更加高效。

BDD是(行為驅動開發 Behavior-Driven Development)指的是不應該針對代碼的實現細節寫測試,而是要針對行為寫測試。BDD測試的是行為,即軟體應該怎樣運行。

  1.      和TDD比起來,BDD是需要我們先寫行為規範(功能明細),在進行軟體開發。功能明細和測試看起來非常相似,但是功能明細更加含蓄一些。BDD採用了更詳細的方式使得它看起來就像是一句話。
  2.      BDD測試應該注重功能而不是實際的結果。你常常會聽說BDD是協助設計軟體,而不是像TDD那樣的測試軟體。

最後總結:TDD的迭代反覆驗證是敏捷開發的保障,但沒有明確如何根據設計產生測試,並保障測試案例的品質,而BDD倡導大家都用簡潔的自然語言描述系統行為的理念,恰好彌補了測試案例(即系統行為)的準確性。

測試架構選擇

利用karma和jasmine來進行ng模組的單元測試。

     Karma:是一個基於Node.js的JavaScript測試執行過程管理工具,這個測試載入器的一個強大特性就是,它可以監控(Watch)檔案的變化,然後自行執行,通過console.log顯示測試結果。

     jasmine是一個行為驅動開發(BDD)的測試架構,不依賴任何js架構以及dom,是一個非常乾淨以及友好API的測試庫.

Karma

karma是一個單元測試的運行控制架構,提供以不同環境來運行單元測試,比如chrome,firfox,phantomjs等,測試架構支援jasmine,mocha,qunit,是一個以nodejs為環境的npm模組.

Karma從頭開始構建,免去了設定測試的負擔,集中精力在應用邏輯上。會產生一個瀏覽器執行個體,針對不同瀏覽器運行測試,同時可以對測試的運行進行一個即時反饋,提供一份debug報告。

測試還會依賴一些Karma外掛程式,如測試覆蓋率Karma-coverage工具、Karman-fixture工具及Karma-coffee處理工具。此外,前端社區裡提供裡比較豐富的外掛程式,常見的測試需求都能涵蓋到。

安裝測試相關的npm模組建議使用—-save-dev參數,因為這是開發相關的,一般的運行karma的話只需要下面兩個npm命令:

npm install karma --save-devnpm install karma-junit-reporter --save-dev

然後一個典型的運行架構通常都需要一個設定檔,在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 start test/karma.conf.js

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); });});
  1.      首先任何一個測試案例以describe函數來定義,它有兩參數,第一個用來描述測試大體的中心內容,第二個參數是一個函數,裡面寫一些真實的測試代碼
  2.      it是用來定義單個具體測試工作,也有兩個參數,第一個用來描述測試內容,第二個參數是一個函數,裡面存放一些測試方法
  3.      expect主要用來計算一個變數或者一個運算式的值,然後用來跟期望的值比較或者做一些其它的事件
  4.      beforeEach與afterEach主要是用來在執行測試工作之前和之後做一些事情,上面的例子就是在執行之前改變變數的值,然後在執行完成之後重設變數的值

開始單元測試

下面分別以控制器,指令,過濾器和服務四個部分來編寫相關的單元測試。項目地址為angular-seed(點我)項目,可以下載demo並運行其測試案例。

demo中是一個簡單的todo應用,會包含一個文本輸入框,其中可以編寫一些筆記,按下按鈕可以將新的筆記加入筆記列表中,其中使用notesfactory封裝LocalStorage來儲存筆記資訊。

先介紹一下angular中測試相關的組件angular-mocks。

瞭解angular-mocks

在Angular中,模組都是通過依賴注入來載入和執行個體化的,因此官方提供了angular-mocks.js測試載入器來提供模組的定義、載入,依賴注入等功能。

其中一些常用的方法(掛載在window命名空間下):

angular.mock.module: module用來載入已有的模組,以及配置inject方法注入的模組資訊。具體使用如下:

beforeEach(module('myApp.filters'));beforeEach(module(function($provide) { $provide.value('version', 'TEST_VER');}));

該方法一般在beforeEach中使用,在執行測試案例之前可以獲得模組的配置。

angular.mock.inject: inject用來注入配置好的ng模組,來供測試案例裡進行調用。具體使用如下:

it('should provide a version', inject(function(mode, version) { expect(version).toEqual('v1.0.1'); expect(mode).toEqual('app'); }));

其實inject裡面就是利用angular.inject方法建立的一個內建的依賴注入執行個體,然后里面的模組和普通的ng模組的依賴處理是一樣的。

Controller部分

Angular模組是todoApp,控制器是TodoController,當按鈕被點擊時,TodoController的createNote()函數會被調用。下面是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中用了個叫做notesFactory的服務來儲存和提取筆記。當createNote()被調用時,會使用這個服務將一條資訊存入LocalStorage中,然後清空當前的note。因此,在編寫測試模組是,應該保證控制器初始化,scope中有一定數量的筆記,在調用createNote()之後,筆記的數量應該加一。

具體的單元測試如下:

describe('TodoController Test', function() { beforeEach(module('todoApp')); // 將會在所有的it()之前運行 // 我們在這裡不需要真正的factory。因此我們使用一個假的factory。 var mockService = { notes: ['note1', 'note2'], //僅僅初始化兩個項目 get: function() { return this.notes; }, put: function(content) { this.notes.push(content); } }; // 現在是真正的東西,測試spec it('should return notes array with two elements initially and then add one', inject(function($rootScope, $controller) { //注入依賴項目 var scope = $rootScope.$new(); // 在建立控制器的時候,我們也要注入依賴項目 var ctrl = $controller('TodoController', {$scope: scope, notesFactory:mockService}); // 初始化的技術應該是2 expect(scope.notes.length).toBe(2); // 輸入一個新項目 scope.note = 'test3'; // now run the function that adds a new note (the result of hitting the button in HTML) // 現在運行這個函數,它將會增加一個新的筆記項目 scope.createNote(); // 期待現在的筆記數目是3 expect(scope.notes.length).toBe(3); }) );});

在beforeEach中,每一個測試案例被執行之前,都需要載入模組module("todoApp")

由於不需要外部以來,因此我們本地建立一個假的mockService來代替factory,用來類比noteFactory,其中包含相同的函數,get()put() 。這個假的factory從數組中載入資料代替localStorage的操作。

在it中,聲明了依賴項目$rootScope$controller,都可以由Angular自動注入,其中$rootScope用來獲得根範圍,$controller用作建立新的控制器。

$controller服務需要兩個參數。第一個參數是將要建立的控制器的名稱。第二個參數是一個代表控制器依賴項目的對象,
$rootScope.$new()方法將會返回一個新的範圍,它用來注入控制器。同時我們傳入mockService作為假factory。
之後,初始化會根據notes數組的長度預測筆記的數量,同時在執行了createNote()函數之後,會改變數組的長度,因此可以寫出兩個測試案例。

Factory部分

factory部分的單元測試代碼如下:

describe('notesFactory tests', function() { var factory; // 在所有it()函數之前運行 beforeEach(function() { // 載入模組 module('todoApp'); // 注入你的factory服務 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; }); }); // 檢查是否有我們想要的函數 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); }); // 檢查是否添加了一條新紀錄 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模組中,實際上的factory會調用localStorage來儲存和提取筆記的項目,但由於我們單元測試中,不需要依賴外部服務去擷取和儲存資料,因此我們要對localStorage.getItem()localStorage.setItem()進行spy操作,也就是利用假函數來代替這兩個部分。

spyOn(localStorage,'setItem')andCallFake()是用來用假函數進行監聽的。第一個參數指定需要監聽的對象,第二個參數指定需要監聽的函數,然後andCallfake這個API可以編寫自己的函數。因此,測試中完成了對localStorage和Object的改寫,使函數可以返回我們自己數組中的值。

在測試案例中,首先檢測新封裝的factory函數是否包含了get()put()這兩個方法,,然後進行factory.put()操作後斷言筆記的數量。

Filter部分

我們添加一個過濾器。truncate的作用是如果傳入字串過長後截取前10位。源碼如下:

todoApp.filter('truncate',function(){ return function(input,length){ return (input.length > length ? input.substring(0,length) : input); }});

所以在單元測試中,可以根據傳入字串的情況斷言產生子串的長度。

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); }); );});

之前已經對斷言進行討論了,值得注意的一點是我們需要在調用過濾器的時候在名稱後面加入Filter,然後正常調用即可。

Directive部分

源碼中的指令部分:

todoApp.directive('customColor', function() { return { restrict: 'A', link: function(scope, elem, attrs) { elem.css({'background-color': attrs.customColor}); } };});

由於指令必須編譯之後才能產生相關的模板,因此我們要引入$compile服務來完成實際的編譯,然後再測試我們想要進行測試的元素。

angular.element()會建立一個jqLite元素,然後我們將其編譯到一個新產生的自範圍中,就可以被測試了。具體測試案例如下:

describe('directive tests',function(){ beforeEach(module('todoApp')); it('should set background to rgb(128, 128, 128)', inject(function($compile,$rootScope) { scope = $rootScope.$new(); // 獲得一個元素 elem = angular.element("<span custom-color=\"rgb(128, 128, 128)\">sample</span>"); // 建立一個新的自範圍 scope = $rootScope.$new(); // 最後編譯HTML $compile(elem)(scope); // 希望元素的背景色和我們所想的一樣 expect(elem.css("background-color")).toEqual('rgb(128, 128, 128)'); }) );});

開始端到端測試

在端到端測試中,我們需要從使用者的角度出發,來進行黑箱測試,因此會涉及到一些DOM操作。將一對組件組合起來然後檢查是否如預想的結果一樣。
在這個demo中,我們類比使用者輸入資訊並按下按鈕的過程,檢測資訊能否被添加到localStorage中。

在E2E測試中,需要引入angular-scenario這個檔案,並且建立一個html作為運行report的展示,在html中包含帶有e2e測試代碼的執行js檔案,在編寫完測試之後,運行該html檔案查看結果。具體的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); }); });

我們在端到端測試過程中,首先導航到我們的主html頁面app/notes.html,可以通過browser.navigateTo()來完成,element.query()函數選擇了ul元素並記錄其中有多少個初始化的項目,存放在oldCount變數中。

然後通過input('note').enter()來鍵入一個新的筆記,然後類比一下點擊操作來檢查是否增加了一個新的筆記(li元素)。然後通過斷言可以將新舊的筆記數進行對比。

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的協助,如果有疑問大家可以留言交流。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.