這篇文章主要介紹了在JavaScript的AngularJS庫中進行單元測試的方法,主要針對AngularJS中的控制器相關,需要的朋友可以參考下
開發人員們都一致認為單元測試在開發項目中十分有好處。它們協助你保證代碼的品質,從而確保更穩定的研發,即使需要重構時也更有信心。
測試驅動開發流程圖
AngularJS的代碼聲稱其較高的可測性確實是合理的。單單文檔中列出端對端的測試執行個體就能說明。就像AngularJS這樣的項目雖然都說單元測試很簡單但真正做好卻不容易。即使官方文檔中以提供了詳盡的執行個體,但在我的實際應用中卻還是很有挑戰。這裡我就簡單示範一下我是怎麼操作的吧.
Instant Karma
Karma 是來Angular團隊針對JavaScript開發的一個測試回合架構。它很方便的實現了自動執行測試工作從而替代了繁瑣的手工操作(好比迴歸測試集或是載入目標測試的依賴關係)Karma 和Angular的協作就好比花生醬和果凍.
只需要在Karma中定義好設定檔啟動它,接下來它就會在預期的測試環境下的自動執行測試案例。你可以在設定檔中制定相關的測試環境。angular-seed,是我強烈推薦的可以快速實施的方案。在我近期的項目中Karma 的配置如下:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
module.exports = function(config) { config.set({ basePath: '../', files: [ 'app/lib/angular/angular.js', 'app/lib/angular/angular-*.js', 'app/js/**/*.js', 'test/lib/recaptcha/recaptcha_ajax.js', 'test/lib/angular/angular-mocks.js', 'test/unit/**/*.js' ], exclude: [ 'app/lib/angular/angular-loader.js', 'app/lib/angular/*.min.js', 'app/lib/angular/angular-scenario.js' ], autoWatch: true, frameworks: ['jasmine'], browsers: ['PhantomJS'], plugins: [ 'karma-junit-reporter', 'karma-chrome-launcher', 'karma-firefox-launcher', 'karma-jasmine', 'karma-phantomjs-launcher' ], junitReporter: { outputFile: 'test_out/unit.xml', suite: 'unit' } }) } |
這個跟angular-seed的預設配置類似只不過有以下幾點不同:
需要更改瀏覽器從Chrome 轉到PhantomJS, 這樣每次跳轉時無需再開啟新的瀏覽器視窗,但在OSX系統會有視窗延遲。所以這個外掛程式還有瀏覽器設定都做了更改。
由於我的應用程式需要引用Google的Recaptcha服務因此添加了依賴的recaptcha_ajax.js小檔案。這個小配置就像在Karma的設定檔中添加一行代碼那麼簡單。
autoWatch確實是個很酷的設定,它會讓Karma在有檔案更改時自動迴歸你的測試案例。你可以這樣安裝Karma:
?
1npm install karma
angular-seed 提供了一個簡單的指令碼inscripts/test.sh去觸發Karma的測試。
用Jasmine設計測試案例
當使用Jasmine----一種行為驅動開發模式的JavaScript測試架構為Angular設計單元測試用例時大部分的資源都已可擷取。
這也就是我接下來要說的話題。
如果你要對AngularJS controller做單元測試可以利用Angular的依賴注入dependency injection 功能匯入測試情境中controller需要的服務版本還能同時檢查預期的結果是否正確。例如,我定義了這個controller去高亮需要導航去的那個頁簽:
?
1 2 3 4 5 |
app.controller('NavCtrl', function($scope, $location) { $scope.isActive = function(route) { return route === $location.path(); }; }) |
如果想要測試isActive方法,我會怎麼做呢?我將檢查$locationservice 變數是否返回了預期值,方法返回的是否預期值。因此在我們的測試說明中我們會定義好局部變數儲存測試過程中需要的controlled版本並在需要時注入到對應的controller當中。然後在實際的測試案例中我們會加入斷言來驗證實際的結果是否正確。整個過程如下:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
describe('NavCtrl', function() { var $scope, $location, $rootScope, createController; beforeEach(inject(function($injector) { $location = $injector.get('$location'); $rootScope = $injector.get('$rootScope'); $scope = $rootScope.$new(); var $controller = $injector.get('$controller'); createController = function() { return $controller('NavCtrl', { '$scope': $scope }); }; })); it('should have a method to check if the path is active', function() { var controller = createController(); $location.path('/about'); expect($location.path()).toBe('/about'); expect($scope.isActive('/about')).toBe(true); expect($scope.isActive('/contact')).toBe(false); }); }); |
使用整個基本的結構,你就能設計各種類型的測試。由於我們的測試情境使用了本地的環境來調用controller,你也可以多加上一些屬性接著執行一個方法清除這些屬性,然後再驗證一下屬性到底有沒有被清除。
$httpBackendIs Cool
那麼要是你在調用$httpservice請求或是發送資料到服務端呢?還好,Angular提供了一種
$httpBackend的mock方法。這樣的話,你就能自訂服務端的響應內容,又或是確保服務端的響應結果能和單元測試中的預期保持一致。
具體細節如下:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
describe('MainCtrl', function() { var $scope, $rootScope, $httpBackend, $timeout, createController; beforeEach(inject(function($injector) { $timeout = $injector.get('$timeout'); $httpBackend = $injector.get('$httpBackend'); $rootScope = $injector.get('$rootScope'); $scope = $rootScope.$new(); var $controller = $injector.get('$controller'); createController = function() { return $controller('MainCtrl', { '$scope': $scope }); }; })); afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); it('should run the Test to get the link data from the go backend', function() { var controller = createController(); $scope.urlToScrape = 'success.com'; $httpBackend.expect('GET', '/slurp?urlToScrape=http:%2F%2Fsuccess.com') .respond({ "success": true, "links": ["http://www.google.com", "http://angularjs.org", "http://amazon.com"] }); // have to use $apply to trigger the $digest which will // take care of the HTTP request $scope.$apply(function() { $scope.runTest(); }); expect($scope.parseOriginalUrlStatus).toEqual('calling'); $httpBackend.flush(); expect($scope.retrievedUrls).toEqual(["http://www.google.com", "http://angularjs.org", "http://amazon.com"]); expect($scope.parseOriginalUrlStatus).toEqual('waiting'); expect($scope.doneScrapingOriginalUrl).toEqual(true); }); }); |
正如你所見,beforeEach call其實都很類似,唯一不同的是我們是從injector擷取$httpBackend而並非直接擷取。即使如此,建立不同的測試時還會有一些明顯的不同之處。對初學者來說,會有一個afterEachcall 方法來確保$httpBackend在每次用例執行後不會有明顯的異常請求。如果你觀察一下測試情境的設定和$httpBackend方法的應用就會會發現有那麼幾點不是那麼直觀的。
實際上調用$httpBackend的方法也算是簡單明了但還不夠——我們還得在傳值給$scope.$apply的方法中把調用封裝到實際測試中的$scope.runTest方法上。這樣在$digest被觸發後才能處理HTTP請求。而如你所見直到我們調用$httpBackend.flush()方法後$httpBackend才會被解析,這也就保證了我們能在調用過程中去驗證返回的結果是否正確(在上面的樣本中,controller的$scope.parseOriginalUrlStatusproperty屬性將被傳遞給調用者,我們也因此能即時監控)
接下來的幾行代碼都是在調用過程中檢測$scopethat屬性的斷言。很酷吧?
提示:在某些單元測試中,使用者習慣把沒有$的範圍標記為變數。這個在Angular文檔中並沒有強制要求或是過分強調,只是我在使用中為了提高可讀性和一致性才使用$scopelike這種方式。
結論
也許這就是我做起來對其他人而言只是自然而然能做到的事情之一,但是學習使用Angular編寫單元測試一開始對我而言確實是相當痛苦的。我發現自己對如何開始的理解大多來自互連網上各種部落格文章和資源的拼拼湊湊,沒有真正一致或明確的最佳實務,而是通過自然而然隨意的選擇。我想針對我最終得到的成果提供一些文檔,以協助那些也許還在坑裡面掙紮的其他人,畢竟他們只是想要編寫代碼而已,而非不得不去瞭解Angular和Jasmine中所有的怪異特性和獨特用法。因此我希望這篇文章能對你有些許協助。