はじめに
AngularJS 公式サイトにあるチュートリアル AngularJS PhoneCat Tutorial App の Step2 で 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 以降にもテストコードが紹介されていますので、実装にかかるより先にテストコードを書くようにすると、正しく実装できたことを実感できて楽しいと思います。