JavaScript
Article

AngularJS Testing: Bootstrap Blocks, Routes, Events, and Animations

By Ravi

In the process of building and delivering full featured software, we apply several techniques to check the correctness and quality of the software. Unit testing is one of these techniques. Many organizations pay a lot of attention towards unit testing as it reduces the cost of finding and fixing potential issues of an application.

As we start developing applications with hundreds of thousands of JavaScript lines, we can’t escape from testing the code. Several JavaScript developers say that testing JavaScript is even more important as behavior of the language is unknown until runtime.

Thankfully, AngularJS makes testing the code written using the framework easier by supporting features like Dependency Injection (DI). In three of my past articles, I discussed a few tips on mocking, how to test controllers, services and providers and how to test directives. This article will cover testing Bootstrap blocks of an AngularJS application (includes config blocks, run blocks and route resolve blocks), scope events and animations.

You can download the code used in this article from our GitHub repo, where you will also find instructions on running the tests.

Testing Config and Run blocks

Config and run blocks are executed at the beginning of the life cycle of a module. They contain important logic that controls the way a module, a widget, or an application works. It’s a bit tricky to test them as they can’t be called directly like other components. At the same time, they can’t be ignored as their role is crucial.

Consider the following config and run blocks:

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);
    });
});

Similarly to the case of testing providers, we need to make sure that the module is loaded before testing the functionality inside the config and run blocks. So, we will use an empty inject block to load the modules.

The following snippet mocks the dependencies used in above block and loads the module:

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());
});

I intentionally didn’t mock the $routeProvider object as we’ll test the registered routes later in this article.

Now that the module is loaded, the config and run blocks have already been executed. So, we can start testing their behavior. As the config block registers routes, we can check if it registered the right routes. We will test if the expected number of routes is registered. The following tests verify the functionality of the config block:

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();
    });
});

The run block in the sample code calls a service and registers an event. We will test the event later in this article. For the moment, let’s test the call to the service method:

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");
    });
});

Testing Scope Events

Event aggregation is one of the good ways to make two objects interact with each other even when them are totally unaware of each other. AngularJS provides this feature through $emit/$broadcast events on $scope. Any object in the application can raise an event or listen to an event depending on the need.

When an application runs, both subscribers and publishers of the events are available. But, as unit tests are written in isolation, we have only one of the objects available in the unit tests. So, the test spec will have to mimic the other end to be able to test the functionality.

Let’s test the event registered in the run block above:

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

The $locationChangeStart event is broadcasted by the $location service whenever the location of the application changes. As already mentioned, we need to manually fire this event and test if the message is sent by the messenger. The following test performs this task:

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);
});

Testing Routes

Routes define the way users navigate the application. Any improper or accidental change in the route configuration will lead to a bad user experience. So, routes should have tests too.

So far, ngRoute and ui-router are the most widely used routers in AngularJS applications. Routes for both of these providers have to be defined in the config block, while route data is made available through services. Route data configured with ngRoute is available through the service $route. Route data of ui-router is available through the service $state. These services can be used to inspect if the right set of routes are configured.

Consider the following config block:

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'
    });
});

Let’s now test these routes. As the first thing, let’s get a reference of the $route service:

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

The /home route above has templateUrl, a controller and a resolve block configured. Let’s write assertions to test them:

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();
});

Test for the details route would be similar. We also have a default route configured using the otherwise block. The default routes are registered with null as the key value. The following is the test for it:

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

Testing Resolve Blocks

Resolve blocks are the factories that are created when a route is loaded and they are accessible to the controller associated with the route. It’s an interesting scenario to test as their scope is limited to the route and we still need to get a reference of the object.

The only way I see to test the resolve block is by invoking it using the $injector service. Once invoked, it can be tested like any other factory. The following snippet tests the resolve block configured with the home route that we created above:

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();
});

I had to mimic the templateUrl in the above test as AngularJS tries to move to the default route when the digest cycle is invoked.

The same approach can be used to test $httpInterceptors as well.

Testing Animations

The technique of testing animations has some similarity with testing directives but testing animations is easier since animations are not as complex as directives.

The angular-mocks library contains the module ngAnimateMock to ease the job of testing animations. This module has to be loaded before testing animations.

Consider the following JavaScript animation:

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);
        }
    };
});

Let’s now write tests to verify the correctness of this animation. We need to load the required modules and get references of the required objects.

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();
    });
});

To test the enter part of the animation defined above, we need to programmatically make an element enter the rootElement referenced in the above snippet.

An important thing to remember before testing animations is that animations are prevented by AngularJS from running till the first digest cycle is completed. This is done to make the initial binding faster. The last statement in the above snippet kicks off the first digest cycle so that we don’t have to do it in every test.

Let’s test the enter animation defined above. It has two test cases:

  1. Element should be positioned at 10px top and 20px left with opacity 0.5 while entering
  2. Element should be positioned at 0px top and 0px left with opacity 1 after 1 sec of entering. This has to be an asynchronous test as the control will have to wait for 1 sec before asserting

The followings are the tests for above two cases:

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);
});

Similarly, for the leave animation we need to check the values of the CSS properties after 100ms. Since the test has to wait for the animation to be completed, we need to make the test asynchronous.

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
});

Conclusion

With this article, I covered most of the testing tips that I learned over the past two years while testing AngularJS code. This is not the end and you will learn a lot more when you write tests for the business scenarios of real applications. I hope you got enough knowledge on testing AngularJS code by now. Why waiting? Just go and write tests for every single line of code you wrote till now!

  • Nico

    Hey :) I’ve wrote something about a very similar subject, you can find the article here: https://blog.axawebcenter.fr/javascript/configurer-ses-routes-sur-une-application-angularjs/ Sorry, it’s only French, but I think you could understand it!

  • Utot Moe

    Wow! The effort is much appreciated more than you know.

    • http://sravi-kiran.blogspot.com Ravi Kiran

      Thanks @utotmo22:disqus :)

  • tinmanic

    This doesn’t work when using ui-router (i.e. $stateProvider) instead of $routeProvider:

    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();
    });

    because there is no route.routes. What’s the equivalent for ui-router?

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in JavaScript, once a week, for free.