karma + Jasmine で AngularJS を TDD する

はじめに

 AngularJS 公式サイトにあるチュートリアル AngularJS PhoneCat Tutorial AppStep2 で Jasmine なテストコードが出てきますが、その記事だけではゼロからテストを実行できないので、今回はこれを実行するまでの手順を明確にしてみたいと思います。
 チュートリアルではテンプレートに関する話題になっていますが、テストコードはコントローラーがきちんと 3 つの Phones を出力できているか、という点をテストしています。
 また、チュートリアルでは実装を先にしていますが、本投稿ではディレクトリ・ファイル構成のスケルトン (空ファイル) の状態からはじめて、TDD で進めたいと思います。はやる気持ちを抑えて、ソースコード、テストコードはまだ書かないでください。
 Jasmine 実行環境はこちらを参考に準備しておきます。

 本投稿での大まかな流れは下記のとおりです。

  • プロジェクトの作成
  • ソース監視の開始
  • テストコード作成と実装

プロジェクトの作成

 チュートリアルでの構成とは少し異なり、下記のようなディレクトリ・ファイル構成となるようにしていきます。

~/work/angularjs-tutorial/step02/        ← プロジェクトのベースディレクトリ
  app/                                   ← Angular アプリのベースディレクトリ
    js/
      angular.min.js
      controllers.js
    index.html
  test/                                  ← テストコードのベースディレクトリ
    js/
      angular-mocks.js
    unit/
      controllersSpec.js
  karma.conf.js                          ← テスト実行時の設定ファイル

Angular アプリのベースディレクトリの準備

 下記コマンドを実行します。
 angular.min.js もダウンロードしておきます。

$ mkdir -p ~/work/angularjs-tutorial/step02/app/js
$ cd ~/work/angularjs-tutorial/step02/app/
$ touch js/controllers.js
$ touch index.html
$ curl -L https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js \
       -o js/angular.min.js
$ cd -

テストコードのベースディレクトリの準備

 下記コマンドを実行します。
 テスト実行には angular-mocks.js も必要になりますので、ここでダウンロードしておきます。

$ mkdir -p ~/work/angularjs-tutorial/step02/test/{js,unit}
$ cd ~/work/angularjs-tutorial/step02/test/
$ curl -L https://raw.githubusercontent.com/angular/bower-angular-mocks/master/angular-mocks.js \
       -o js/angular-mocks.js
$ touch unit/controllersSpec.js
$ cd -

テスト実行設定 初期化

 上記のディレクトリ・ファイル構成に対してテストが実行できるように、karma init コマンドを実行して設定ファイルを生成します。
 テスト対象となるソースコードとテスト対象の実行に必要なソースコード (app/js/*.js)、テスト実行時のみに必要な依存ファイル (test/js/*.js)、テストコード (test/**/*Spec.js) のすべてを指定します。

$ cd ~/work/angularjs-tutorial/step02/
$ karma init

Which testing framework do you want to use ?
Press tab to list possible options. Enter to move to the next question.
> jasmine

Do you want to use Require.js ?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no

Do you want to capture any browsers automatically ?
Press tab to list possible options. Enter empty string to move to the next question.
> Chrome
> 

What is the location of your source and test files ?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Enter empty string to move to the next question.
> app/js/*.js
> test/js/*.js
> test/**/*Spec.js
> 

Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string to move to the next question.
> 

Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> yes


Config file generated at "/home/tester/work/angularjs-tutorial/step02/karma.conf.js".

ソース監視の開始

 一度、この状態でテストを実行しましょう。
 まだテストコードがないため、テスト数 0 のエラーが表示されるはずです。
 ここが実装のスタート地点となります。

$ karma start

INFO [karma]: Karma v0.12.31 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 31.0.1650 (Linux)]: Connected on socket 2xTvNu24LKSEI6vEvhY2 with id 54245001
Chrome 31.0.1650 (Linux): Executed 0 of 0 ERROR (0.022 secs / 0 secs)

テストコード作成と実装

 いよいよ実装にとりかかります。が、まずはテストコードを書いてからです。
 とはいえ、メインのモジュールやコントローラーがないと期待しないエラーが発生するので、最低限の実装だけ済ませてしまいます。

app/js/controllers.js
var phonecatApp = angular.module('phonecatApp', []);

phonecatApp.controller('PhoneListCtrl', function () {
});

 テストコードを先に書くと、実装がまだないはずですので FAIL となります。FAIL したことを確認した後、次のステップではじめて実装に移ります。
 実装後に、テストが PASS すれば実装完了です。

テストコード作成

 チュートリアルにあるとおり、$scope 経由で phones から3つの phone オブジェクトが取得できるか、という内容のテストコードを作成します。

test/unit/controllersSpec.js
describe('PhoneListCtrl -', function () {
    beforeEach(module('phonecatApp'));

    it(  'should create "phones" model with 3 phones'
       , inject(function ($controller) {
        var scope = {}
          , ctrl = $controller('PhoneListCtrl', {$scope: scope});

        expect(scope.phones.length).toBe(3);
    }));
});

 このテストコードを保存したタイミングで、テストが実行されて、下記のように 1 件 FAIL したことを確認できたでしょうか。
 PhoneListCtrl は 3 つの phone オブジェクトを(当然ですが)返さなかったので、FAIL になりました。

INFO [watcher]: Changed file "/home/tester/work/angularjs-tutorial/step02/app/js/controllers.js".
Chrome 31.0.1650 (Linux) PhoneListCtrl - should create "phones" model with 3 phones FAILED
	TypeError: Cannot read property 'length' of undefined
	    at Object. (/home/tester/work/angularjs-tutorial/step02/test/unit/controllersSpec.js:10:28)
	    at Object.e [as invoke] (/home/tester/work/angularjs-tutorial/step02/app/js/angular.min.js:36:315)
	    at Object.workFn (/home/tester/work/angularjs-tutorial/step02/test/js/angular-mocks.js:2420:20)
	Error: Declaration Location
	    at window.inject.angular.mock.inject (/home/tester/work/angularjs-tutorial/step02/test/js/angular-mocks.js:2391:25)
	    at Suite. (/home/tester/work/angularjs-tutorial/step02/test/unit/controllersSpec.js:6:10)
	    at /home/tester/work/angularjs-tutorial/step02/test/unit/controllersSpec.js:1:1
Chrome 31.0.1650 (Linux): Executed 1 of 1 (1 FAILED) ERROR (0.076 secs / 0.041 secs)

ソースコード実装

 3 つの phone オブジェクトを返すよう実装します。

app/js/controllers.js
var phonecatApp = angular.module('phonecatApp', []);

phonecatApp.controller('PhoneListCtrl', ['$scope', function ($scope) {
    $scope.phones = [{
        'name': 'Nexus S',
        'snippet': 'Fast just got faster with Nexus S.'
    }, {
        'name': 'Motorola XOOM™ with Wi-Fi',
        'snippet': 'The Next, Next Generation tablet.'
    }, {
        'name': 'MOTOROLA XOOM™',
        'snippet': 'The Next, Next Generation tablet.'
    }];
}]);

 これも、保存したらすぐさまテストが実行されます。
 下記のように、無事 PASS を確認できました。

INFO [watcher]: Changed file "/home/tester/work/angularjs-tutorial/step02/app/js/controllers.js".
Chrome 31.0.1650 (Linux): Executed 1 of 1 SUCCESS (0.078 secs / 0.052 secs)

おわりに

 今回はテスト自体の解説ではなく、テスト実行の流れに焦点をあてて紹介しました。
 テストの粒度をもっと細かくすれば、信頼性の高い実装になるでしょうし、他にコントローラーが増えたり、機能が増えたりしても、テストコードも一緒に「育てて」いくことで、品質をどんどん高めていくことができます。
 チュートリアル Step3 以降にもテストコードが紹介されていますので、実装にかかるより先にテストコードを書くようにすると、正しく実装できたことを実感できて楽しいと思います。

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください