AngularJSのユニットテストを書く[DI活用編]


スクリーンショット 2016-05-13 18.23.07

1.angular-mocksを依存関係に追加

bowerなりnpmなりでangular-mocksを入れて、karma.conf.jsのfilesにangular-mocksを使用するように追記


files: [
            'bower_components/angular/angular.js',
            'bower_components/angular-mocks/angular-mocks.js',
            'src/**/*.js',
            'test/**/*.js'
        ],

これで、テストコード内でmodule()やinject()でDIしたコンポーネントを取り出せるようになる。

2.テストコードを書いてみる
テスト対象のプロダクトコードを適当に書きます

一応、Controller、Service、directive、filterを網羅してみます
controller

angular.module('app').controller('MyController', ['$scope', function ($scope) {
    this.name = $scope.name;
    this.count = 0;
    this.countUp = function () {
        this.count++;
    }

}]);

directive

angular.module('app').directive('myAwesomeComponent', function () {
    return {
        restrict: 'E',
        replace: true,
        template: '<p>Very Very Nice Component</p>'
    }
});

filter

angular.module('app').filter('NotANumber', function () {
    return function (value) {
        return angular.isNumber(value) ? value : 'NaN';
    }
});

service

angular.module('app').service('FizzBuzzService', function () {
    this.doFizzBuzz = function (number) {
        if (number % 15 === 0) {
            return 'FizzBuzz';
        } else if (number % 3 === 0) {
            return 'Fizz';
        } else if (number % 5 === 0) {
            return 'Buzz'
        } else {
            return number;
        }
    };
});

対応したテストコードを書きます。angular-mockのmodule()、inject()で対象のインスタンスを取り出して一つ一つアサーションを書いていく、って流れが基本です
di_test.js

describe('DIのテスト', function () {
    //appモジュールを読み込む
    beforeEach(module('app'));

    describe('filterのテスト', function () {
        var $filter;
        //inject関数を呼び出すことで、appの$filterをインジェクションできる
        beforeEach(inject(function (_$filter_) {
            $filter = _$filter_;
        }));
        it('数字ならそのまま、数字以外ならNaNと表示するフィルタ', function () {
            var nanFilter = $filter('NotANumber'); //NotANumberフィルターを取得
            expect(nanFilter(1)).toEqual(1); //テストの実行
            expect(nanFilter('hoge')).toEqual('NaN');
        })

    });

    describe('Serviceのテスト', function () {
        var fizzbuzz;
        // _service名_ とすることで、対応したServiceを取り出すことができる
        beforeEach(inject(function (_FizzBuzzService_) {
            fizzbuzz = _FizzBuzzService_;
        }));
        it('fizzbuzzのテスト', function () {
            expect(fizzbuzz.doFizzBuzz(3)).toEqual('Fizz');
            expect(fizzbuzz.doFizzBuzz(5)).toEqual('Buzz');
            expect(fizzbuzz.doFizzBuzz(15)).toEqual('FizzBuzz');
            expect(fizzbuzz.doFizzBuzz(1)).toEqual(1);
        })
    });

    describe('controllerのテスト', function () {
        var $rootScope;
        var $controller;
        var myController;
        beforeEach(inject(function (_$rootScope_, _$controller_) {
            $rootScope = _$rootScope_;
            $controller = _$controller_;
        }));
        it('MyControllerのテスト', function () {
            var scope = {
                name: 'foo'
            };
            //引数に$scopeを渡すことで、その$scopeでコントローラを初期化
            myController = $controller('MyController', {$scope: scope});
            expect(myController.count).toEqual(0);
            myController.countUp();
            expect(myController.count).toEqual(1);
            expect(myController.name).toEqual('foo');
        })
    });

    describe('directiveのテスト', function () {
        var $compile;
        var $rootScope;
        beforeEach(inject(function (_$compile_, _$rootScope_) {
            $compile = _$compile_;
            $rootScope = _$rootScope_;
        }));
        it('ディレクティブが作成できる', function () {
            var elem = $compile('<my-awesome-component></my-awesome-component>')($rootScope);
            $rootScope.$digest();//バインディングを処理
            expect(elem.html()).toContain('Very Very Nice Component');
        })
    });
});

状態を持たないFilter,Serviceのテストは比較的容易ですが状態を持つController,Directiveのテストは難しいですね。
状態ごとにテストをする必要があるのと状態をインジェクションするコードを書かないといけないのでどうしてもコード量が長くなります。