使用AngularJS编写各种UI单元测试
首先
※使用的是Angular 1x版本。
通常的逻辑测试可以轻松编写,但是编写UI单元测试就较为困难。
然而,我们往往会把AngularJS模块的测试放在后面。
接下来,我将描述如何编写Controller、Component、Service等的测试方法。
此外,还描述了一些稍微有点不易理解的测试写作方式。
-
- $httpのレスポンスをMockするテストの書き方も記述しています。
-
- $timeoutを使っているメソッドのテスト
- $qを使っているメソッドのテスト
前提 tí)
能够在使用 Karma 和 Jasmine 进行测试的环境中执行测试。
我们将在以下准备的 GitHub 项目中进行测试。
https://github.com/chibi929/angularjs-test-sample
执行环境
请参考 GitHub 上的 package.json 文件。
顺便提一下,Node 版本是 6.9.5。
{
...
"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 模块
对控制器进行测试
测试用例代码
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);
考试代码
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);
});
});
});
组件的测试
测试代码
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());
测试代码
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');
});
});
});
过滤器的测试
测试目标代码
import * as angular from 'angular';
function replace() {
return (input, s1, s2) => {
return input.replace(s1, s2);
}
}
angular.module('chibiApp', []).filter('replace', replace);
测试代码
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');
});
});
服务的测试
测试目标代码
import * as angular from 'angular';
class CurrentTimeService {
public readonly className = "CurrentTimeService";
public now(): Date {
return new Date();
}
}
angular.module('chibiApp', []).service('currentTime', CurrentTimeService);
测试代码
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通信的响应进行测试。
考虑测试的代码
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);
考试代码
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的方法的测试
考试代码
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);
考试代码
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的方法。
测试目标代码
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);
考试代码
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();
});
});
});
总结
希望能把這個當作我的秘籍,
把它複製黏貼之後,再逐漸完善測試代碼,
如果能夠這樣使用就太好了…