之前有记录过如何搭建基于Karma, Jasmine的测试环境, 这段时间陆续也写了不少Angular的单元测试, 在这里针对不同类型的代码测试做个记录吧. 以后当模板使用好了.
Interceptor
以下的代码块是测试 typhoon.member
模块里的 accessTokenInterceptor
, 这个拦截器的作用是给需要验证用户身份的请求加上 accessToken. 需要注意如何注入 $httpProvider 以及如何模拟 authService.
describe('typhoon.member', function() {
var accessTokenInterceptor, authService, $httpProvider;
var token = '7428ac81a0dba1f8b01f44a00cffbf56cb2e4170';
beforeEach(module('typhoon.member', function(_$httpProvider_) {
$httpProvider = _$httpProvider_;
}));
beforeEach(function() {
module(function($provide) {
$provide.factory('authService', function() {
return {
getToken: jasmine.createSpy('getToken').and.returnValue(token)
};
});
});
});
beforeEach(inject(function(_accessTokenInterceptor_, _authService_) {
authService = _authService_;
accessTokenInterceptor = _accessTokenInterceptor_;
}));
describe('accessTokenInterceptor', function() {
var url, config;
it('should be defined', function() {
expect(accessTokenInterceptor).toBeDefined();
});
it('should be added as an interceptor', function() {
expect($httpProvider.interceptors).toContain('accessTokenInterceptor');
});
it('should have a handler for request', function() {
expect(angular.isFunction(accessTokenInterceptor.request)).toBeTruthy();
});
});
describe('when request an URL that need authentication', function() {
beforeEach(function() {
url = 'API_SERVER/user/info';
config = { url: url };
accessTokenInterceptor.request(config);
});
it('should append accessToken to url', function() {
expect(config.params.accessToken).toBe(token);
});
});
});
Service
以下代码块是测试 typhoon.message
模块里的 messageService
. 这个服务提供了全局的消息管理. 需要注意 如何使用 $timeout 对 persistence 进行测试.
describe('typhoon.message', function () {
var messageServiceProvider, messageService, $timeout;
beforeEach(module('typhoon.message', function (_messageServiceProvider_) {
messageServiceProvider = _messageServiceProvider_;
}));
beforeEach(inject(function (_messageService_, _$timeout_) {
messageService = _messageService_;
$timeout = _$timeout_;
}));
describe('messageService', function () {
describe('.append(String message)', function () {
beforeEach(function () {
messageService.append('something is wrong');
});
it('should have one message in stack', function () {
expect(messageService.has()).toBeTruthy();
expect(messageService.all().length).toEqual(1);
expect(messageService.all()[0].type).toEqual(messageService.defaults.messageType);
});
describe('after 3 seconds', function () {
beforeEach(function () {
$timeout.flush();
});
it('should clear all message', function () {
expect(messageService.has()).toBeFalsy();
expect(messageService.all().length).toEqual(0);
});
});
});
describe('.append(message, {persistence: true})', function () {
beforeEach(function () {
messageService.append('something is wrong', {
persistence: true
});
});
it('should have one message in stack', function () {
expect(messageService.has()).toBeTruthy();
expect(messageService.all().length).toEqual(1);
expect(messageService.all()[0].type).toEqual(messageService.defaults.messageType);
});
describe('after 3 seconds', function () {
it('should still have one message in stack', function () {
expect(messageService.has()).toBeTruthy();
expect(messageService.all().length).toEqual(1);
});
it('no pending tasks that need to be flushed', function () {
expect($timeout.verifyNoPendingTasks()).toBeFalsy();
});
});
});
...
});
describe('messageServiceProvider', function () {
it('should be defined', function () {
expect(messageServiceProvider).toBeDefined();
});
...
});
});
Controller
以下代码块是测试 typhoon.login
模块的 SignInController
. 登陆页面控制器. 我们要测试提交按钮的状态切换, 登陆功能, 成功后的路由跳转, 以及失败后的错误提示. 需要注意如何使用 $state
进行路由跳转测试.
/**
* Created by Administrator on 2016/8/9.
*/
describe('SignInController', function () {
var $controller, $scope, $state, $rootScope, signInAPIService, authService;
var signInSuccess;
beforeEach(function () {
module('ui.router');
module('typhoon.login');
module(function ($stateProvider) {
$stateProvider.state('app', {abstract: true});
$stateProvider.state('app.dash', {url: '/'});
});
module(function ($provide) {
$provide.factory('signInAPIService', function () {
return {
signin: jasmine.createSpy('signin').and.callFake(function (arg, success, fail) {
signInSuccess ? success({
access_token: '62a5947ab2aba9eae57e48e3a5b3459d644f06c7'
}) : fail();
})
};
});
$provide.factory('authService', function ($q) {
var deferred = $q.defer();
deferred.resolve({});
return {
setToken: jasmine.createSpy('setToken'),
identity: jasmine.createSpy('identity').and.returnValue(deferred.promise)
};
});
});
inject(function (_$rootScope_, _$controller_, _$state_, _signInAPIService_, _authService_, _sessionService_) {
$state = _$state_;
$rootScope = _$rootScope_;
$controller = _$controller_;
$scope = _$rootScope_.$new();
signInAPIService = _signInAPIService_;
authService = _authService_;
});
function initController() {
return $controller('MemberSignInController', {
$scope: $scope
});
}
describe('.disableLoginBtn()', function () {
it('when signin form invalid, save button should be disabled', function () {
$scope.formSignin = {
$invalid: true
};
initController();
expect($scope.disableLoginBtn()).toBeTruthy();
});
it('when captcha not filled, save button should be disabled', function () {
$scope.formSignin = {
$invalid: false
};
$scope.captcha = {
getCaptchaData: function () {
return {
valid: false
}
}
};
initController();
expect($scope.disableLoginBtn()).toBeTruthy();
});
it('when captcha filled, and signin form valid, save button should be enabled', function () {
$scope.formSignin = {
$invalid: false
};
$scope.captcha = {
getCaptchaData: function () {
return {
valid: true
}
}
};
initController();
expect($scope.disableLoginBtn()).toBeFalsy();
});
});
describe('.signIn() successfully', function () {
beforeEach(function () {
$scope.login = {
name: 'mock',
password: 'mock'
};
$scope.captcha = {
getCaptchaData: function () {
return {
valid: true,
name: '23848b1e0f0de95a4bc9',
value: '1f213b76d3b89c12b43c'
}
}
};
signInSuccess = true;
initController();
$scope.signIn();
});
it('signInAPIService.signin should be called', function () {
expect(signInAPIService.signin).toHaveBeenCalled();
});
it('signInAPIService.signin should post username, password, and captcha information', function () {
expect(signInAPIService.signin).toHaveBeenCalledWith(jasmine.objectContaining({
username: 'mock',
password: 'mock',
'23848b1e0f0de95a4bc9': '1f213b76d3b89c12b43c'
}), jasmine.any(Function));
});
it('should set token and identity', function () {
expect(authService.setToken).toHaveBeenCalled();
expect(authService.identity).toHaveBeenCalled();
expect(sessionService.setToken).toHaveBeenCalled();
});
it('should go home page', function () {
$rootScope.$apply();
expect($state.current.name).toBe('app.home');
});
});
...
});
});
Directive
以下代码块是测试 typhoon.upload
模块里的 uploadDirective
. 这是一个用来做图片上传的指令. 我们要测试指令编译后的HTML结构以及功能是否正确. 需要注意如何使用 $compile 去编译指令.
describe('typhoon.upload', function () {
var $scope, $element, uploadService;
beforeEach(module('typhoon.upload'));
beforeEach(inject(function ($rootScope, $compile, _uploadService_) {
uploadService = _uploadService_;
$scope = $rootScope.$new();
$scope.vm = {
files: [],
errors: []
};
$element = $compile('<upload files="vm.files" errors="vm.errors" max="9"></upload>')($scope);
$rootScope.$digest();
}));
describe('uploadDirective', function () {
var controller;
beforeEach(function () {
controller = $element.controller('upload');
spyOn(uploadService, 'upload').and.callFake(function (file, resolve, reject, progress) {
if (file.success) {
resolve({
data: {
id: 'fas989f78s7d',
url: 'url'
}
});
}
else {
reject({
name: 'name', status: -100, message: 'something is wrong!'
});
}
progress(0.5);
});
});
it('controller should have properties as config', function () {
expect(controller).toBeDefined();
expect(controller.files).toBeDefined();
expect(controller.errors).toBeDefined();
expect(controller.max).toEqual('9');
});
it('controller should have upload function', function () {
expect(controller.upload).toBeDefined();
});
it('should have correct dom', function () {
expect($element[0].tagName).toEqual('BUTTON');
expect($element.attr('ngf-select')).toBeDefined();
});
describe('after select file', function () {
var files = [{name: 'picture.png'}];
beforeEach(function () {
controller.upload(files);
});
it('should call uplodService to upload', function () {
expect(uploadService.upload).toHaveBeenCalled();
expect(uploadService.upload.calls.count()).toBe(1);
});
it('should update progress', function () {
expect(files[0].progress).toBe(50);
});
});
describe('after select invalide file', function () {
beforeEach(function () {
controller.upload([], [{name: 'error.png', $error: 'tooBig'}]);
});
it('should not call uplodService to upload', function () {
expect(uploadService.upload).not.toHaveBeenCalled();
});
it('should push invalid file in errors', function () {
expect(controller.errors).toEqual([{
name: 'error.png',
msg: 'tooBig'
}]);
});
});
describe('files number exceed the max', function () {
beforeEach(function () {
controller.files.length = 9;
controller.upload([{}]);
});
it('should not call uplodService to upload', function () {
expect(uploadService.upload).not.toHaveBeenCalled();
});
});
describe('upload success', function () {
beforeEach(function () {
controller.upload([{success: true}]);
});
it('should put file id and url in files', function () {
expect(controller.files).toEqual([
{
id: 'fas989f78s7d',
url: 'url'
}
]);
});
});
describe('upload fail', function () {
beforeEach(function () {
controller.upload([{success: false}]);
});
it('should put file id and url in files', function () {
expect(controller.errors).toEqual([
{
name: 'name',
msg: '-100: something is wrong!'
}
]);
});
});
});
});