はじめに

前回はごく簡単なAngularJSサービスをJasmineでBDDする手法を記しました。今回は予告通りにDIを用いたAngularJSサービスのテストを同様に扱っていきたいと思います。

今回の肝はユニットテストにはお馴染みのモックを仕込むことにあります。

開発プロセス例

1. 設計

前回のHelloServiceを拡張することにしましょう。MyServices.HelloService.say(name)を ‘name’ を引数として与えると ‘Hello N-A-M-E’ という文字列を返すメソッドに変更します。

ここで「小文字を大文字にしてハイフンで分割する」機能は別のサービスとして設けることにします。MyUtilServices モジュールに UpperDashService サービスを設けて先述の機能を format(str) というメソッドで実現することにしましょう。

2. 実装

ではさっそくこの振舞をテストコードに落としこむことから始めてみます。

1. メソッドのテストを変更

// helloSpec.js
describe('MyServicesのテスト', function() {
    beforeEach(function () {
        module('MyServices');
    });

    it('HelloService のテスト', inject(function(HelloService) {
-        expect(HelloService.say('Quartet')).toEqual('Hello Quartet');
+        expect(HelloService.say('Quartet')).toEqual('Hello Q-U-A-R-T-E-T');
    }));
});

以下のメッセージが表示されテストはエラーとなりましたね。

Expected 'Hello Quartet' to equal 'Hello Q-U-A-R-T-E-T'.

ここではテストファーストではありませんが、のちのモックのテストの書き方を実感しやすくするために先に実装を行います。

2. 別モジュールのサービスを利用したメソッドの実装

// myServices.js
- angular.module('MyServices', [])
-     .service('HelloService', function() {
+ angular.module('MyServices', ['MyUtilServices'])
+     .service('HelloService', ['UpperDashService', function(UpperDashService) {
         this.say = function(name) {
-             return 'Hello ' + name;
+             var upperDashedName = UpperDashService.format(name);
+             return 'Hello ' + upperDashedName;
         }
-     })
+     }])
;

これでいいはずです。しかしもちろんテストはエラーです。さきほどとエラーの内容は変わっていますね。MyUtilServicesというモジュールがないことが元凶のようです。

Error: [$injector:modulerr] Failed to instantiate module MyServices due to: Error: [$injector:modulerr] Failed to instantiate module MyUtilServices due to: Error: [$injector:nomod] Module 'MyUtilServices' is not available! You either misspelled the module name or forgot to load it.

これに見合ったテストを書いていきましょう。

3. 空のモジュールを作成

まずはMyUtilServicesという空のモジュールをつくってしまいます。これは angular.module の記述ですね。 前回登場した angular.mock.module とは異なるので注意です。

// helloSpec.js
describe('MyServicesのテスト', function() {
    beforeEach(function () {
        module('MyServices');
+        angular.module('MyUtilServices', []);
    });

    it('HelloService のテスト', inject(function(HelloService) {
        expect(HelloService.say('Quartet')).toEqual('Hello Q-U-A-R-T-E-T');
    }));
});

この時点ではまだ以下のようなエラーが出ます。HelloServiceが使用しているUpperDashServiceが提供されていないと言われていますね。

Error: [$injector:unpr] Unknown provider: UpperDashServiceProvider <- UpperDashService <- HelloService

4. 依存サービスのモックを作成

// helloSpec.js
describe('MyServicesのテスト', function() {
    beforeEach(function () {
        module('MyServices');
-        angular.module('MyUtilServices', []);
+        angular.module('MyUtilServices', [])
+            .service('UpperDashService', function() {
+                this.format = function(name) {
+                    return 'Q-U-A-R-T-E-T';
+                };
+            })
+        ;
    });

    it('HelloService のテスト', inject(function(HelloService) {
        expect(HelloService.say('Quartet')).toEqual('Hello Q-U-A-R-T-E-T');
    }));
});

これでテストが通るようになりました。 さて、ここでもうちょっとテストを堅牢にしていきます。

5. メソッドを監視

HelloService.say(name)をコールした時に内部では狙い通りにUpperDashService.format(str)はコールされているでしょうか? ここでJasmineの機能を利用します。 モックのformatメソッドを監視するための記述を加えました。

describe('MyServicesのテスト', function() {
    beforeEach(function() {
        module('MyServices');
        angular.module('MyUtilServices', [])
            .service('UpperDashService', function() {
                this.format = function(name) {
                    return 'Q-U-A-R-T-E-T';
                };
            })
        ;
    });

    it('HelloService のテスト', inject(function(HelloService) {
        expect(HelloService.say('Quartet')).toEqual('Hello Q-U-A-R-T-E-T');
    }));

+    it('HelloService のテスト。UpperDashServiceがコールされているか', inject(function(HelloService, UpperDashService) {
+        spyOn(UpperDashService, 'format');
+        HelloService.say('dummy');
+        expect(UpperDashService.format).toHaveBeenCalled();
+        expect(UpperDashService.format).toHaveBeenCalledWith('dummy');
+    }));
});

最後に

テスト対象が他のサービスに依存している時のテストを可能にするために今回はモックを作成してみました。これで依存関係にあるサービスが未実装でもテストを作成して開発を進められますね。

AngularJSは同じ目的を達成するために複数の書き方(シンタックスシュガー)があるので当エントリとは異なった書き方もWeb上には散見されると思います。ご自身のプロジェクトの目的やポリシーや慣れなどを基準にしっくりくるものを採用されると良いかと思います。

次回はAngularJSにビルトインされているいくつかのサービスを使用したアプリのテストについて記述してみようと思います。

ちなみに当エントリのコードはこちらにありますのでご参考まで。

http://plnkr.co/edit/EGPSyt