AngularJs directive 的單元測試方法

第一次翻譯技術文章,確定不少語句很生疏,有看官的話就見諒,沒有的話也沒人看的到這句話。。javascript

翻譯自:Unit Testing an AngularJS Directivehtml

在這篇文章中,我將詳述如何給咱們上週開發的stepper directive作單元測試的過程。下週會講到如何使用Github和Bower進行組件分離。java

單元測試是一種測試你的項目中每一個最小單元代碼的藝術,是使你的程序思路清晰的基礎。一旦全部的測試經過,這些零散的單元組合在一塊兒也會運行的很好,由於這些單元的行爲已經被獨立的驗證過了。node

單元測試可以避免你的代碼出現迴歸性BUG提升代碼的質量和可維護性使你的代碼在代碼庫中是可信賴的,從而提升團隊合做的質量,使重構變得簡單和快樂: )jquery

單元測試的另外一個用處是當你發現了一個新的BUG,你能夠爲這個BUG寫一個單元測試,當你修改了你的代碼,使這個測試能夠PASS了的時候,就說明這個BUG已經被修復了。git

AngularJS最好的小夥伴兒KarmaJS test runner(一個可以在瀏覽器中運行測試同時生成結果日誌的Node.js server)還有 Jasmine(定義了你的測試和斷言的語法的庫)。咱們使用Grunt-karma將karma集成在咱們經典且繁重的grunt 工做流中,而後在瀏覽器中運行測試。這裏值得注意的是,karma能夠將測試運行在遠程的雲瀏覽器中,好比SauceLabsBrowserStackangularjs

AngularJS是將是通過了嚴密地測試的,因此趕忙給本身點個贊,如今就開始寫測試吧!github


術語:chrome

在咱們進行下一步以前有一些術語須要說明:npm

  • spec: 你想要測試的代碼的說明,包括一個或多個測試條件。spec應該覆蓋全部預期行爲。
  • test suite: 一組測試的集合,定義在Jasmine提供的describe語句塊中,語句塊是能夠嵌套的。
  • test: 測試說明,寫在Jasmin提供的it語句塊中,以一個或者多個指望值結束(譯者按:也就是說,一個it語句塊中,必定要有一個以上的指望值)。
  • actual: 在你的指望中要被測試的值。
  • expected value: 針對測試出的真實值作比較的指望值。(原文:this is the value you test the actual value against.)
  • matcher: 一個返回值爲Boolean類型的函數,用於比較真實值跟指望值。結果返回給jasmine,好比toEqual,toBeGreatherThan,toHaveBeenCalledWith... 你也能夠定義你本身的matcher。
  • expectation: 使用expect函數測試一個值,獲得它的返回值,expectation是與一個獲得指望值的matcher函數連接的。(原文:Use the expect function to test a value, called the actual. It is chained with a matcher function, which takes the expected value.)
  • mock: 一種「stubbed」(不會翻譯)服務,你能夠製造一些假數據或方法來替代程序真正運行時所產生的數據。

這有一個spec文件的例子:


// a test suite (group of tests) //一組測試 describe('sample component test', function() { // a single test //單獨的測試 it('ensure addition is correct', function() { // sample expectation // 簡單的指望 expect(1+1).toEqual(2); // `--- the expected value (2) 指望值是2 // `--- the matcher method (equality) toEqual方法就是matcher函數 // `-- the actual value (2) 真實值是2 }); // another test // 另外一個測試 it('ensure substraction is correct', function() { expect(1-1).toEqual(0); }); });

測試環境搭建

將grunt-karma添加到你項目的依賴中

npm install grunt-karma --save -dev

建立一個karma-unit.js文件

這裏是一個karma-unit文件的例子
這個文件定義了以下內容:
* 將要被加載到瀏覽器進行測試的JS文件。一般狀況下,不只項目用的庫和項目自己的文件須要包含在內,你所要測試的文件和mock文件也要在這裏加載。
* 你想將測試運行在哪款瀏覽器中。
* 怎樣接收到測試結果,是命令行裏仍是在瀏覽器中...?
* 可選插件。

如下是files這一項的例子:

files: [
  "http://code.angularjs.org/1.2.1/angular.js",       <-- angular sourc
  "http://code.angularjs.org/1.2.1/angular-mocks.js", <-- angular mocks & test utils
  "src/angular-stepper.js",                           <-- our component source code
  "src/angular-stepper.spec.js"                       <-- our component test suite
]

注:這裏能夠添加jquery在裏面,若是你須要它幫助你編寫測試代碼(更強大的選擇器,CSS測試,尺寸計算…)

將karma grunt tasks添加到Gruntfile.js中

karma: {
    unit: {
        configFile: 'karma-unit.js',
        // run karma in the background
        background: true,
        // which browsers to run the tests on
        browsers: ['Chrome', 'Firefox']
    }
}

而後建立 angular-stepper.spec.js文件,將上面寫的簡單的測試代碼粘貼進來。這時你就能夠輕鬆運行grunt karma任務去觀察你的測試在瀏覽器中運行而且在命令行中生成測試報告。

....
Chrome 33.0.1712 (Mac OS X 10.9.0): Executed 2 of 2 SUCCESS (1.65 secs / 0.004 secs)
Firefox 25.0.0 (Mac OS X 10.9): Executed 2 of 2 SUCCESS (2.085 secs / 0.006 secs)
TOTAL: 4 SUCCESS

上面有四個點,每一個點都表明一個成功的測試,這時你能夠看到,兩個測試分別運行在咱們配置的兩個瀏覽器中了。
哦也~

那麼接下來,讓咱們寫一些真正的測試代碼吧: )


給directive編寫單元測試

爲咱們的組件所編寫的一組單元測試,又叫作spec的東西,不只應該覆蓋咱們所要測試的組件的全部預期行爲,還要將邊緣狀況覆蓋到(好比不合法的輸入、服務器的異常情況)。

下面展現的angular-stepper組件的測試集的精華部分,完整版點這裏。咱們對這樣一個組件的測試很是簡單,不須要假數據。惟一比較有技巧性的是,咱們將咱們的directive包含在了一個form表單下,這樣可以在使用ngModelController和更新表單驗證正確性的狀況下正確的運行測試。(注:此處的內容須要讀angular-stepper那個組件的文件才能懂爲什麼要將directive包含在form表單中,若是不想深刻了解,能夠忽略這句。原文:The only tricky thing is that we wrap our directive inside a form to be able to test that it plays well with ngModelController and updates form validity correctly.)


// the describe keyword is used to define a test suite (group of tests) describe('rnStepper directive', function() { // we declare some global vars to be used in the tests var elm, // our directive jqLite element scope; // the scope where our directive is inserted // load the modules we want to test 在跑測試以前將你要測試的模塊引入進來 beforeEach(module('revolunet.stepper')); // before each test, creates a new fresh scope // the inject function interest is to make use of the angularJS // dependency injection to get some other services in our test inject方法的做用是利用angularJS的依賴注入將咱們所須要的服務注入進去 // here we need $rootScope to create a new scope 須要用$rootScope新建一個scope beforeEach(inject(function($rootScope, $compile) { scope = $rootScope.$new(); scope.testModel = 42; })); function compileDirective(tpl) { // function to compile a fresh directive with the given template, or a default one // compile the tpl with the $rootScope created above // wrap our directive inside a form to be able to test // that our form integration works well (via ngModelController) // our directive instance is then put in the global 'elm' variable for further tests if (!tpl) tpl = '<div rn-stepper ng-model="testModel"></div></form>'; tpl = '<form name="form">' + tpl + '</form>'; //原文最後一個標籤是</tpl>感受是筆誤。 // inject allows you to use AngularJS dependency injection // to retrieve and use other services inject(function($compile) { var form = $compile(tpl)(scope); elm = form.find('div'); }); // $digest is necessary to finalize the directive generation //$digest 方法對於生成指令是必要的。 scope.$digest(); } describe('initialisation', function() { // before each test in this block, generates a fresh directive beforeEach(function() { compileDirective(); }); // a single test example, check the produced DOM it('should produce 2 buttons and a div', function() { expect(elm.find('button').length).toEqual(2); expect(elm.find('div').length).toEqual(1); }); it('should check validity on init', function() { expect(scope.form.$valid).toBeTruthy(); }); }); it('should update form validity initialy', function() { // test with a min attribute that is out of bounds // first set the min value scope.testMin = 45; // then produce our directive using it compileDirective('<div rn-stepper min="testMin" ng-model="testModel"></div>'); // this should impact the form validity expect(scope.form.$valid).toBeFalsy(); }); it('decrease button should be disabled when min reached', function() { // test the initial button status compileDirective('<div rn-stepper min="40" ng-model="testModel"></div>'); expect(elm.find('button').attr('disabled')).not.toBeDefined(); // update the scope model value scope.testModel = 40; // force model change propagation scope.$digest(); // validate it has updated the button status expect(elm.find('button').attr('disabled')).toEqual('disabled'); }); // and many others... });

一些須要注意的點:

在要被測試的scope中,一個directive須要被compiled(譯者注:也就是上面代碼中的$compile(tpl)(scope);這句話在作的事情)。
一個非隔離scope能夠經過element.scope()方法訪問到。
一個隔離的scope能夠經過element.isolateScope()方法訪問到。

爲啥我在改變一個Model的值的時候須要調用scope.$digest()方法?

在一個真正的angular應用中,\$digest方法是angular經過各類事件(click,inputs,requests...)的反應自動調用的。自動化測試不是以真實的用戶事件爲基礎的,因此咱們須要手動的調用\$digest方法($digest方法負責更新全部數據綁定)。


額外福利 #1: 實時測試

多虧了grunt,當咱們的文件改動的時候,能夠自動的進行測試。

若是你想在你的代碼有任何改動的時候都進行一次測試,只要將一段代碼加入到grunt的watch任務中就行。

js: {
    files: ['src/*.js'],
    tasks: ['karma:unit:run', 'build']
},

你也能夠將grunt的默認任務設置成這樣:

grunt.registerTask('default', ['karma:unit', 'connect', 'watch']);

設置完後,運行grunt,就能夠實時的在內置的server中跑測試了。


額外福利 #2:添加測試覆蓋率報告

做爲開發者,咱們但願以靠譜的數據做爲依據,咱們也但願持續的改進本身的代碼。"coverage"指的是你的測試代碼的覆蓋率,它能夠提供給你一些指標和詳細的信息,無痛的增長你的代碼的覆蓋率。

下面是一個簡易的覆蓋率報告:

覆蓋率報告

咱們能夠詳細的看到每一個文件夾的每一個文件的代碼是否被測試覆蓋。歸功於grunt+karma的集成,這個報告是實時更新的。咱們能夠在每個文件中一行一行的檢查那一塊帶按摩沒有被測試。這樣能使測試變得更加的簡單。

100% test coverage 不表明你的代碼就沒有BUG了,但它表明這代碼質量的提升!

karma+grunt的集成特別的簡單,karma有一套「插件」系統,它容許咱們經過配置karma-unit.js文件來外掛fantastic Istanbul 代碼覆蓋率檢測工具。只要配置一下文件,媽媽就不再用擔憂個人代碼覆蓋率了。

Add coverage to karma

# add the necessary node_modules
npm install karma-coverage --save-dev

如今將新的設置更新到kamar的配置文件中

// here we specify which of the files we want to appear in the coverage report
preprocessors: {
    'src/angular-stepper.js': ['coverage']
},
// add the coverage plugin
plugins: [ 'karma-jasmine', 'karma-firefox-launcher', 'karma-chrome-launcher', 'karma-coverage'],
// add coverage to reporters
reporters: ['dots', 'coverage'],
// tell karma how you want the coverage results
coverageReporter: {
  type : 'html',
  // where to store the report
  dir : 'coverage/'
}

更多覆蓋率的設置請看這裏:https://github.com/karma-runner/karma-coverage

相關文章
相關標籤/搜索