はじめに
※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();
});
});
});
まとめ
自分用のチートシートとして、
コピペしてからテストコードを成長させていく感じで使えたらいいなぁ...