AngularJS测试:引导块,路由,事件和动画

在构建和交付功能全面的软件的过程中,我们应用了多种技术来检查软件的正确性和质量。 单元测试是这些技术之一。 许多组织都对单元测试给予了极大的关注,因为它降低了发现和修复应用程序潜在问题的成本。

随着我们开始使用成千上万的JavaScript行开发应用程序,我们无法逃脱测试代码的麻烦。 几个JavaScript开发人员说,测试JavaScript甚至更为重要,因为直到运行时该语言的行为都是未知的。

幸运的是,AngularJS通过支持诸如依赖注入(DI)之类的功能,使使用该框架编写的代码的测试变得更加容易。 在过去的三篇文章中,我讨论了有关模拟如何测试控制器,服务和提供程序以及如何测试指令一些技巧 。 本文将介绍测试AngularJS应用程序的Bootstrap块(包括配置块,运行块和路由解析块),作用域事件和动画。

您可以从我们的GitHub repo下载本文中使用的代码,您还将在其中找到有关运行测试的说明。

测试配置和运行块

Config和Run块在模块生命周期的开始执行。 它们包含控制模块,小部件或应用程序工作方式的重要逻辑。 测试它们有些棘手,因为它们不能像其他组件一样直接调用。 同时,它们的作用至关重要,因此不可忽视。

考虑以下配置和运行块:

angular.module('configAndRunBlocks', ['ngRoute'])
    .config(function ($routeProvider) {
    $routeProvider.when('/home', {
        templateUrl: 'home.html',
        controller: 'HomeController',
        resolve: {
            bootstrap: ['$q', function ($q) {
                return $q.when({
                    prop: 'value'
                });
            }]
        }
    })
        .when('/details/:id', {
        templateUrl: 'details.html',
        controller: 'DetailsController'
    })
        .otherwise({
        redirectTo: '/home'
    });
})
    .run(function ($rootScope, messenger) {

    messenger.send('Bootstrapping application');
    $rootScope.$on('$locationChangeStart', function (event, next, current) {
        messenger.send('Changing route to ' + next + ' from ' + current);
    });
});

与测试提供程序的情况类似,我们需要在测试config和run块中的功能之前确保已加载模块。 因此,我们将使用一个空的注入块来加载模块。

以下代码段模拟了上面代码块中使用的依赖项并加载了模块:

describe('config and run blocks', function () {
    var routeProvider, messenger;

    beforeEach(function () {
        module('ngRoute');

        module(function ($provide, $routeProvider) {
            routeProvider = $routeProvider;
            spyOn(routeProvider, 'when').andCallThrough();
            spyOn(routeProvider, 'otherwise').andCallThrough();

            messenger = {
                send: jasmine.createSpy('send')
            };
            $provide.value('messenger', messenger);
        });

        module('configAndRunBlocks');
    });

    beforeEach(inject());
});

我故意没有嘲笑$routeProvider对象,因为我们将在本文后面测试注册的路由。

现在已经加载了模块,配置和运行块已经执行。 因此,我们可以开始测试其行为。 当config块注册路由时,我们可以检查它是否注册了正确的路由。 我们将测试预期的路由数是否已注册。 以下测试验证了config块的功能:

describe('config block tests', function () {
    it('should have called registered 2 routes', function () {
        //Otherwise internally calls when. So, call count of when has to be 3
        expect(routeProvider.when.callCount).toBe(3);
    });

    it('should have registered a default route', function () {
        expect(routeProvider.otherwise).toHaveBeenCalled();
    });
});

示例代码中的run块调用服务并注册一个事件。 我们将在本文后面测试该事件。 目前,让我们测试对service方法的调用:

describe('run block tests', function () {
    var rootScope;
    beforeEach(inject(function ($rootScope) {
        rootScope = $rootScope;
    }));
    it('should send application bootstrap message', function () {
        expect(messenger.send).toHaveBeenCalled();
        expect(messenger.send).toHaveBeenCalledWith("Bootstrapping application");
    });
});

测试范围事件

事件聚合是使两个对象相互交互的好方法之一,即使它们完全不相互了解。 AngularJS通过$scope上的$scope $emit / $broadcast事件提供此功能。 应用程序中的任何对象都可以根据需要引发事件或侦听事件。

当应用程序运行时,事件的订阅者和发布者均可用。 但是,由于单元测试是独立编写的,因此单元测试中只有一个可用的对象。 因此,测试规范将必须模仿另一端才能测试功能。

让我们测试一下上面的运行块中注册的事件:

$rootScope.$on('$locationChangeStart', function (event, next, current) {
    messenger.send('Changing route to ' + next + ' from ' + current);
});

每当应用程序的位置更改时, $location服务都会广播$locationChangeStart事件。 如前所述,我们需要手动触发此事件并测试消息是否由Messenger发送。 以下测试执行此任务:

it('should handle the $locationChangeStart event', function () {
    var next = '/second';
    var current = '/first';
    rootScope.$broadcast('$locationChangeStart', next, current);
    expect(messenger.send).toHaveBeenCalled();
    expect(messenger.send).toHaveBeenCalledWith('Changing route to ' + next + ' from ' + current);
});

测试路线

路由定义了用户浏览应用程序的方式。 路由配置的任何不当或意外更改都将导致不良的用户体验。 因此,路线也应进行测试。

到目前为止, ngRouteui-router是AngularJS应用程序中使用最广泛的路由器。 这两个提供程序的路由都必须在config块中定义,而路由数据可通过服务获得。 通过ngRoute配置的路由数据可通过服务$route 。 ui-router的路由数据可通过服务$state 。 这些服务可用于检查是否配置了正确的路由集。

考虑以下配置块:

angular.module('configAndRunBlocks', ['ngRoute'])
    .config(function ($routeProvider) {
    $routeProvider.when('/home', {
        templateUrl: 'home.html',
        controller: 'HomeController',
        resolve: {
            bootstrap: ['$q', function ($q) {
                return $q.when({
                    prop: 'value'
                });
            }]
        }
    })
        .when('/details/:id', {
        templateUrl: 'details.html',
        controller: 'DetailsController'
    })
        .otherwise({
        redirectTo: '/home'
    });
});

现在让我们测试这些路线。 首先,让我们获取$route服务的参考:

beforeEach(inject(function ($route) {
    route = $route;
}));

上面的/home路由配置了templateUrl ,一个控制器和一个resolve块。 让我们编写断言来测试它们:

it('should have home route with right template, controller and a resolve block', function () {
    var homeRoute = route.routes['/home'];
    expect(homeRoute).toBeDefined();
    expect(homeRoute.controller).toEqual('HomeController');
    expect(homeRoute.templateUrl).toEqual('home.html');
    expect(homeRoute.resolve.bootstrap).toBeDefined();
});

测试详细路线将是相似的。 我们还具有使用else块配置的默认路由。 默认路由注册为null作为键值。 以下是对其的测试:

it('should have a default route', function () {
    var defaultRoute = route.routes['null'];
    expect(defaultRoute).toBeDefined();
});

测试解析块

解析块是在加载路由时创建的工厂,与该路由关联的控制器可以访问它们。 这是一个有趣的场景,因为它们的范围仅限于路由,我们仍然需要获取该对象的引用。

我看到测试resolve块的唯一方法是使用$injector服务调用它。 一旦被调用,就可以像其他工厂一样进行测试。 以下代码段测试了使用我们在上面创建的本地路由配置的resolve块:

it('should return data on calling the resolve block', function () {
    var homeRoute = route.routes['/home'];
    var bootstrapResolveBlock = homeRoute.resolve.bootstrap;
    httpBackend.expectGET('home.html').respond('<div>This is the homepage!</div>');
    var bootstrapSvc = injector.invoke(bootstrapResolveBlock); //[1].call(q);
    bootstrapSvc.then(function (data) {
        expect(data).toEqual({
            prop: 'value'
        });
    });
    rootScope.$digest();
    httpBackend.flush();
});

我不得不在上面的测试中模仿templateUrl ,因为当摘要循环被调用时AngularJS会尝试移动到默认路由。

同样的方法也可以用来测试$httpInterceptors

测试动画

测试动画的技术与测试指令有些相似,但是测试动画更容易,因为动画不像指令那么复杂。

angular-mocks库包含ngAnimateMock模块,以简化测试动画的工作。 测试动画之前必须先加载此模块。

考虑以下JavaScript动画:

angular.module('animationsApp', ['ngAnimate']).animation('.view-slide-in', function () {
    return {
        enter: function (element, done) {
            element.css({
                opacity: 0.5,
                position: "relative",
                top: "10px",
                left: "20px"
            })
                .animate({
                top: 0,
                left: 0,
                opacity: 1
            }, 500, done);
        },
        leave: function (element, done) {
            element.animate({
                opacity: 0.5,
                top: "10px",
                left: "20px"
            }, 100, done);
        }
    };
});

现在让我们编写测试以验证此动画的正确性。 我们需要加载所需的模块并获取所需对象的引用。

beforeEach(function () {
    module('ngAnimate', 'ngAnimateMock', 'animationsApp');
    inject(function ($animate, $rootScope, $rootElement) {
        $animate.enabled(true);
        animate = $animate;
        rootScope = $rootScope;
        rootElement = $rootElement;
        divElement = angular.element('<div class="view-slide-in">This is my view</div>');
        rootScope.$digest();
    });
});

要测试上面定义的动画的enter部分,我们需要以编程方式使元素输入上述代码段中引用的rootElement

在测试动画之前要记住的重要一点是,AngularJS会阻止动画运行到第一个摘要循环完成。 这样做是为了使初始绑定更快。 上面代码段中的最后一条语句开始了第一个摘要周期,因此我们不必在每个测试中都这样做。

让我们测试上面定义的输入动画。 它有两个测试用例:

  1. 输入时,元素应位于顶部10像素和左侧20像素,且不透明度为0.5
  2. 输入1秒钟后,元素应位于顶部0px和左侧0px且不透明度为1。 这必须是一个异步测试,因为控件必须在声明前等待1秒钟

以下是以上两种情况的测试:

it('element should start entering from bottom right', function () {
    animate.enter(divElement, rootElement);
    rootScope.$digest();

    expect(divElement.css('opacity')).toEqual('0.5');
    expect(divElement.css('position')).toEqual('relative');
    expect(divElement.css('top')).toEqual('10px');
    expect(divElement.css('left')).toEqual('20px');
});

it('element should be positioned after 1 sec', function (done) {
    animate.enter(divElement, rootElement);
    rootScope.$digest();

    setTimeout(function () {
        expect(divElement.css('opacity')).toEqual('1');
        expect(divElement.css('position')).toEqual('relative');
        expect(divElement.css('top')).toEqual('0px');
        expect(divElement.css('left')).toEqual('0px');
        done();
    }, 1000);
});

同样,对于请假动画,我们需要在100毫秒后检查CSS属性的值。 由于测试必须等待动画完成,因此我们需要使测试异步。

it('element should leave by sliding towards bottom right for 100ms', function (done) {
    rootElement.append(divElement);
    animate.leave(divElement, rootElement);
    rootScope.$digest();
    setTimeout(function () {
        expect(divElement.css('opacity')).toEqual('0.5');
        expect(divElement.css('top')).toEqual('10px');
        expect(divElement.css('left')).toEqual('20px');
        done();
    }, 105);
    //5 ms delay in the above snippet is to include some time for the digest cycle
});

结论

在本文中,我介绍了过去两年在测试AngularJS代码时学到的大多数测试技巧。 这还不是终点,为真实应用程序的业务场景编写测试时,您将学到更多。 我希望您到现在为止对测试AngularJS代码有足够的了解。 为什么要等待? 只需为您到目前为止编写的每一行代码编写测试!

From: https://www.sitepoint.com/angularjs-testing-tips-bootstrap-blocks-routes-events-animations/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值