Angular 单元测试模板

之前有记录过如何搭建基于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!'
                    }
                ]);
            });
        });
    });
});

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容