Posted at

AngularJSで各種UIのユニットテストを書く

More than 1 year has passed since last update.


はじめに

※Angular 1xです。

通常のロジックのテストは問題なく書けるとして、UIのユニットテストを書くのは難しい。

とはいえ、後回しにしがちだが、AngularJSのモジュールのテストも書きたい。

ControllerやComponent, Service などのテストの書き方を記述していきます。

その他、以下のように若干わかりづらいテストの書き方も記述しています。


  • $httpのレスポンスをMockするテストの書き方も記述しています。

  • $timeoutを使っているメソッドのテスト

  • $qを使っているメソッドのテスト


前提

Karma と Jasmine を使ったテストが実行できる環境であること。

今回は以下の GitHub に用意したプロジェクトで行います。

https://github.com/chibi929/angularjs-test-sample


実行環境

GitHub の package.json をご参照下さい。

ちなみに Node は 6.9.5 です。


package.json

{

...
"dependencies": {
"angular": "^1.6.5"
},
"devDependencies": {
"@types/angular": "^1.6.27",
"@types/angular-mocks": "^1.5.10",
"@types/jasmine": "^2.5.53",
"angular-mocks": "^1.6.5",
"extract-text-webpack-plugin": "^3.0.0",
"istanbul-instrumenter-loader": "^2.0.0",
"jasmine": "^2.6.0",
"karma": "^1.7.0",
"karma-coverage": "^1.1.1",
"karma-coverage-istanbul-reporter": "^1.3.0",
"karma-html-reporter": "^0.2.7",
"karma-jasmine": "^1.1.0",
"karma-junit-reporter": "^1.2.0",
"karma-phantomjs-launcher": "^1.0.4",
"karma-sourcemap-loader": "^0.3.7",
"karma-typescript-preprocessor": "^0.3.1",
"karma-webpack": "^2.0.4",
"ts-loader": "^2.3.1",
"typescript": "^2.4.2",
"webpack": "^3.4.0"
}
...
}


Angular モジュールのテスト


Controller のテスト


テスト対象コード


テスト対象コード.ts

import * as angular from 'angular';

class SampleController implements ng.IController {
public readonly className = "SampleController";
private clickCount = 0;

public click() {
this.clickCount++;
}

public getClickCount() {
return this.clickCount;
}
}
angular.module('chibiApp', []).controller('sampleController', SampleController);



テストコード


テストコード.ts

describe("SampleControllerのテスト", () => {

let $controller;
beforeEach(angular.mock.module('chibiApp'));
beforeEach(angular.mock.inject((_$controller_) => {
$controller = _$controller_;
}));

describe('コンストラクタのテスト', () => {
it('変数className', () => {
const controller = $controller('sampleController');
expect(controller.className).toEqual("SampleController");
});
});

describe('click()のテスト', () => {
it('clickCountが増えていること', () => {
const controller = $controller('sampleController');
expect(controller.getClickCount()).toEqual(0);
controller.click();
expect(controller.getClickCount()).toEqual(1);
controller.click();
expect(controller.getClickCount()).toEqual(2);
});
});
});



Component のテスト


テスト対象コード


テスト対象コード.ts

import * as angular from 'angular';

class SampleComponentOptions implements ng.IComponentOptions {
public controller = ComponentController;
public bindings = {
firstName: '@',
lastName: '@'
};
}

class ComponentController implements ng.IComponentController {
public firstName: string;
public lastName: string;

public getFullName(): string {
return this.firstName + " " + this.lastName;
}
}

angular.module('chibiApp', []).component('sampleComponent', new SampleComponentOptions());



テストコード


テストコード.ts

describe("SampleComponentのテスト", () => {

let $componentController;
beforeEach(angular.mock.module('chibiApp'));
beforeEach(angular.mock.inject((_$componentController_) => {
$componentController = _$componentController_;
}));

describe('インスタンス変数のテスト', () => {
it('bindしてないとき', () => {
const component = $componentController('sampleComponent', null);
expect(component.firstName).toBeUndefined();
expect(component.lastName).toBeUndefined();
});

it('bindしているとき', () => {
let bindings = {
firstName: 'chibi',
lastName: 'kinoko'
};
const component = $componentController('sampleComponent', null, bindings);
expect(component.firstName).toEqual('chibi');
expect(component.lastName).toEqual('kinoko');
});
});

describe('getFullName()のテスト', () => {
it('フルネームで返却されること', () => {
let bindings = {
firstName: 'chibi',
lastName: 'kinoko'
};
const component = $componentController('sampleComponent', null, bindings);
expect(component.getFullName()).toEqual('chibi kinoko');
});
});
});



Filter のテスト


テスト対象コード


テスト対象コード.ts

import * as angular from 'angular';

function replace() {
return (input, s1, s2) => {
return input.replace(s1, s2);
}
}
angular.module('chibiApp', []).filter('replace', replace);



テストコード


テストコード.ts

describe('replaceのテスト', () => {

let $filter;
beforeEach(angular.mock.module('chibiApp'));
beforeEach(angular.mock.inject((_$filter_) => {
$filter = _$filter_;
}));

it('置換できること', () => {
const replace = $filter('replace');
expect(replace('abbccc', 'a', 'z')).toEqual('zbbccc');
expect(replace('abbccc', 'bb', 'z')).toEqual('azccc');
expect(replace('abbccc', 'ccc', 'z')).toEqual('abbz');
});
});



Service のテスト


テスト対象コード


テスト対象コード.ts

import * as angular from 'angular';

class CurrentTimeService {
public readonly className = "CurrentTimeService";
public now(): Date {
return new Date();
}
}
angular.module('chibiApp', []).service('currentTime', CurrentTimeService);



テストコード


テストコード.ts

describe("CurrentTimeServiceのテスト", () => {

let currentTime;
beforeEach(angular.mock.module('chibiApp'));
beforeEach(angular.mock.inject((_currentTime_) => {
currentTime = _currentTime_;
}));

describe('インスタンス変数のテスト', () => {
it('変数className', () => {
expect(currentTime.className).toEqual("CurrentTimeService");
});
});

describe('now()のテスト', () => {
it('現在時刻が取得できる', () => {
expect(currentTime.now().getDay()).toEqual(new Date().getDay());
});
});
});



HTTP通信のレスポンスをMockしてテスト


テスト対象コード


テスト対象コード.ts

import * as angular from 'angular';

class HttpSampleController {
public readonly className = "HttpSampleController";
public firstName: string;
public lastName: string;

constructor(private $http: ng.IHttpService) {
}

public request(): void {
this.$http.get('/data').then((res: any) => {
this.firstName = res.data.first;
this.lastName = res.data.last;
});
}
}
angular.module('chibiApp', []).controller('httpSampleController', HttpSampleController);



テストコード


テストコード.ts

describe('HttpSampleControllerのテスト', () => {

let $controller;
let $httpBackend;
beforeEach(angular.mock.module('chibiApp'));
beforeEach(angular.mock.inject((_$controller_, _$httpBackend_) => {
$controller = _$controller_;
$httpBackend = _$httpBackend_;
$httpBackend.whenGET('/data').respond(200, {first: 'chibi', last: 'kinoko'});
}));

describe('コンストラクタのテスト', () => {
it('変数className', () => {
const controller = $controller('httpSampleController');
expect(controller.className).toEqual("HttpSampleController");
});
});

describe('#getのテスト', () => {
it('HTTP通信のレスポンスを取得できていること', () => {
const controller = $controller('httpSampleController');
controller.request();
$httpBackend.flush();

expect(controller.firstName).toEqual('chibi');
expect(controller.lastName).toEqual('kinoko');
});
});
});



$timeoutを使っているメソッドのテスト


テスト対象コード


テスト対象コード.ts

import * as angular from 'angular';

class TimeoutSampleController implements angular.IController {
public readonly className = "TimeoutSampleController";
public firstName: string;
public lastName: string;

constructor(private $timeout: ng.ITimeoutService) {
}

public request(): void {
this.$timeout(() => {
this.firstName = "chibi";
this.lastName = "kinoko";
}, 1000);
}

public request2(): ng.IPromise<any> {
return this.$timeout(() => {
this.firstName = "chibi";
this.lastName = "kinoko";
}, 1000);
}
}
angular.module('chibiApp', []).controller('timeoutSampleController', TimeoutSampleController);



テストコード


テストコード.ts

describe('TimeoutSampleControllerのテスト', () => {

let $controller;
let $timeout;
beforeEach(angular.mock.module('chibiApp'));
beforeEach(angular.mock.inject((_$controller_, _$timeout_) => {
$controller = _$controller_;
$timeout = _$timeout_;
}));

describe('コンストラクタのテスト', () => {
it('変数className', () => {
const controller = $controller('timeoutSampleController');
expect(controller.className).toEqual("TimeoutSampleController");
});
});

describe('#requestのテスト', () => {
it('名前が取得できていること', () => {
const controller = $controller('timeoutSampleController');
controller.request();
expect(controller.firstName).toBeUndefined();
expect(controller.lastName).toBeUndefined();
$timeout.flush();
expect(controller.firstName).toEqual('chibi');
expect(controller.lastName).toEqual('kinoko');
});
});

describe('#request2のテスト', () => {
it('名前が取得できていること', (done) => {
const controller = $controller('timeoutSampleController');
controller.request2().then(() => {
expect(controller.firstName).toEqual('chibi');
expect(controller.lastName).toEqual('kinoko');
done();
});
expect(controller.firstName).toBeUndefined();
expect(controller.lastName).toBeUndefined();
$timeout.flush();
});
});
});



$qを使っているメソッドのテスト


テスト対象コード


テスト対象コード.ts

import * as angular from 'angular';

class QSampleController implements ng.IController {
public readonly className = "QSampleController";

constructor(private $q: ng.IQService) {
}

public request(resolveFlg: boolean): ng.IPromise<any> {
const deferred = this.$q.defer();
if (resolveFlg) {
deferred.resolve({first: "chibi", last: "kinoko"});
} else {
deferred.reject({first: "undefined-chibi", last: "undefined-kinoko"});
}
return deferred.promise;
}
}
angular.module('chibiApp', []).controller('qSampleController', QSampleController);



テストコード


テストコード.ts

describe('QSampleControllerのテスト', () => {

let $controller;
let $rootScope;
let $q;
beforeEach(angular.mock.module('chibiApp'));
beforeEach(angular.mock.inject((_$controller_, _$rootScope_, _$q_) => {
$controller = _$controller_;
$rootScope = _$rootScope_;
$q = _$q_;
}));

describe('コンストラクタのテスト', () => {
it('変数className', () => {
const controller = $controller('qSampleController');
expect(controller.className).toEqual("QSampleController");
});
});

describe('#requestのテスト', () => {
it('名前が取得できること: true', (done) => {
const controller = $controller('qSampleController');
controller.request(true).then((res) => {
expect(res.first).toEqual('chibi');
expect(res.last).toEqual('kinoko');
done();
});
$rootScope.$apply();
});

it('名前が取得できないこと: false', (done) => {
const controller = $controller('qSampleController');
const deferred = $q.defer();
controller.request(false).then((res) => {
fail("Not come here.");
done();
}, (err) => {
expect(err.first).toEqual('undefined-chibi');
expect(err.last).toEqual('undefined-kinoko');
done();
});
$rootScope.$apply();
});
});
});



まとめ

自分用のチートシートとして、

コピペしてからテストコードを成長させていく感じで使えたらいいなぁ...