AngularJS test 新手上路 (2) - 環境介紹

剛開始學習 AngularJS 時,循一般的網路上學習路徑,你可能會先 run 一次官方的 tutorial,完全照著裡面寫的 step 做就可以完成一個包含 test 的完整小專案。雖然它一步一步介紹的很清楚,但其實仔細看會發現 test 的部分接近於貼了程式碼就馬虎帶過 lol

來看看做為 tutorial 範例的 angular-phonecat 在 step2 的時候提供的 controller unit test spec:

/* file: test/unit/controllersSpec.js */

describe('PhoneCat controllers', function() {
  beforeEach(module('phonecatApp'));
 
  describe('PhoneListCtrl', function(){
 
    it('should create "phones" model with 3 phones', inject(function($controller) {
      var scope = {},
          ctrl = $controller('PhoneListCtrl', { $scope: scope });
 
      expect(scope.phones.length).toBe(3);
    }));
  });
});

那些 describe, beforeEach … 是什麼? tutorial 完全沒寫它們的來源啊~~~

Jasmine (test framework)

Jasmine 是 AngularJS 預設的 test framework (AngularJS 本身是用這套 test framework 來做 test 的意思)。

Jasmine 提供了測試 JavaScript 需要的各種工具,稍稍瞄一眼上面連結的 documentation,這邊列出比較常在 AngularJS 的 test 裡面會看到的關鍵字:

或許你會有疑問,為什麼這些東西看起來好像是 global 的?因為它們確實是 global 的。

在 run Jasmine test 的時候其實完全不在 AngularJS 本身的 context 裡面,因此你需要想方設法設定周圍的環境,讓測試的對象感覺(?)自己是身在 AngularJS 的 context 裡,並且各種需要的 serivce 也都能夠手到擒來。

至於到底 run Jasmine test 的環境是怎麼樣的?我們來看一下 standalone Jasmine (在瀏覽器裡跑的) 的 SpecRunner 的組成是如何:

<!-- file: SpecRunner.html -->

<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script>

<!-- include source files here... -->
<script type="text/javascript" src="src/Player.js"></script>
<script type="text/javascript" src="src/Song.js"></script>

<!-- include spec files here... -->
<script type="text/javascript" src="spec/SpecHelper.js"></script>
<script type="text/javascript" src="spec/PlayerSpec.js"></script>

<script type="text/javascript">
  (function() {
    var jasmineEnv = jasmine.getEnv();
    jasmineEnv.updateInterval = 1000;

    var htmlReporter = new jasmine.HtmlReporter();

    jasmineEnv.addReporter(htmlReporter);

    jasmineEnv.specFilter = function(spec) {
      return htmlReporter.specFilter(spec);
    };

    var currentWindowOnload = window.onload;

    window.onload = function() {
      if (currentWindowOnload) {
        currentWindowOnload();
      }
      execJasmine();
    };

    function execJasmine() {
      jasmineEnv.execute();
    }

  })();
</script>

如果我們把一開始說的 angular-phonecat 的 unit test 擺進去這個 setup:

<!-- file: SpecRunner.html -->

<!-- include source files here... -->
<script type="text/javascript" src="lib/angular/angular.js"></script>
<script type="text/javascript" src="lib/angular-mocks/angular-mocks.js"></script>
<script type="text/javascript" src="../app/js/controllers.js"></script>

<!-- include spec files here... -->
<script type="text/javascript" src="unit/controllersSpec.js"></script>

恍然大悟! 其實寫 spec 就跟你平常直接寫在 HTML 裡的 <script> tag 裡沒兩樣,只是前面載入了 Jasmine 跟 AngularJS 罷了。

當然你也可以依據個人喜好選擇別的 test framework,比如說 Mocha 也是非受歡迎的 framework 之一。另外也請務必去逛一下 Chai 的網站,瞭解一下什麼是 assertion。

ngMock (angular-mocks.js)

ngMock 是 AngularJS 用來協助你進行 test 的 module,ngMock 裡面提供到 global 的 function 有三個:

  • dump - 協助你在 test 的時候可以 print 出 AngularJS 相關 context 的內容
  • module - 插入一段 configuration phase 的程式碼,也可以用來指定 module dependency
  • inject - 好用的 injector function,方便你在需要的時候叫出某 service

除了這三個 global function 之外,ngMock 還另外做了一些 骯髒 便民的服務來協助完成 AngularJS test 的工作,讓我們來看看這段在 angular-mocks.js 裡面的程式碼:

angular.module('ngMock', ['ng']).provider({
  $browser: angular.mock.$BrowserProvider,
  $exceptionHandler: angular.mock.$ExceptionHandlerProvider,
  $log: angular.mock.$LogProvider,
  $interval: angular.mock.$IntervalProvider,
  $httpBackend: angular.mock.$HttpBackendProvider,
  $rootElement: angular.mock.$RootElementProvider
}).config(['$provide', function($provide) {
  $provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
}]);

默默的把 AngularJS 內建 module ng 裡的一部份 service 換掉,並對 $timeout 做了一些 decoration。於是在 test 有引入 angular-mocks.js 的環境下,上述這些 service 的功能都變成了 test 結果檢驗取向的。

以取代過的 $log 為例,變成了原本所有 $log.xxx() 會吐出的訊息都被累積到對應的陣列裡面存著,方便你在 test 的 spec 中對 log message 做檢查。

所有變型的 serivce 功能都在官方 API doc 裡面有介紹。

Karma (test runner)

還記得在上一篇文章安裝的 Karma 嗎? 它可不是這個 Karma 喔! Karma 原本是叫做 Testacular,後來才改的名字,也是 AngularJS team 出品的 open source project,它在整個 test 流程裡面扮演了一個不可或缺的角色。

在上面 Jasmine 的部分我們曾經有看到一個 SpecRunner.html 的內容,可是我們的 project 資料夾裡根本沒有這個檔案,到底是怎麼樣讓 test 跑起來的? 其實這都要歸功於 Karma, test runner 的工作就是幫你設置好 test framework 需要的環境,好讓 test 的流程順利完成。

大致上 Karma 幫我們做的事情有下面幾項:

  1. 找到指定的檔案們 (source, test spec) 與指定的 test framework (已安裝 jasmine)
  2. 將 1. 的檔案們組到一個暫時的 HTML 檔裡
  3. 同時開啟設定要跑 test 的瀏覽器們來跑 test
  4. 跑完關掉瀏覽器,把暫時的 HTML 檔案清除
  5. 透過設定 autoWatchsingleRun,可以在檔案有更新的時候自動循環上面 1 ~ 3 步驟,即時看到 test 結果

當然其實除了 Karma 你也有別的選擇。像 Testem 就在 console 下提供了一個比較有 GUI 感覺的結果顯示方式,也有不少人在使用。

下期預告

老實說因為在 test 方面我也是新手 Orz 所以下期開始將會是筆記性質的 - unit test 筆記。

一起在 test 的道路上努力前進吧!

系列文章

AngularJS test 新手上路 (1) - 基本環境設定 AngularJS test 新手上路 (2) - 環境介紹