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 modulengAnimateMock
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:
- Element should be positioned at 10px top and 20px left with opacity 0.5 while entering
- 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
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!Frequently Asked Questions (FAQs) on AngularJS Testing Tips, Bootstrap Blocks, Routes, Events, and Animations
How can I effectively test my AngularJS application?
Testing is a crucial part of any application development process. For AngularJS, you can use tools like Jasmine and Karma. Jasmine is a behavior-driven development framework for testing JavaScript code. It does not depend on any other JavaScript frameworks. Karma is a test runner for JavaScript that runs on Node.js. It is suitable for testing AngularJS or any other JavaScript project. To effectively test your AngularJS application, write tests that cover all the functions and components of your application. Use Jasmine to write your tests and Karma to run them.
What are Bootstrap Blocks and how can I use them in my AngularJS application?
Bootstrap Blocks are pre-designed sections of a webpage, such as headers, footers, or content sections, that you can use to quickly build your website. They are built using the Bootstrap framework, a popular HTML, CSS, and JavaScript framework for developing responsive, mobile-first websites. To use Bootstrap Blocks in your AngularJS application, you can use directives to create custom HTML elements that represent the Bootstrap Blocks. Then, you can use these custom elements in your AngularJS views.
How can I handle routes in my AngularJS application?
Routing is the process of directing a user’s request to the correct page. In AngularJS, you can handle routes using the $routeProvider service. This service allows you to define routes for your application. Each route corresponds to a view and a controller. When a user navigates to a specific URL, the $routeProvider will load the corresponding view and controller.
How can I handle events in my AngularJS application?
Events in AngularJS are actions or occurrences such as mouse clicks, key presses, or data changes. You can handle events in AngularJS using directives such as ng-click, ng-keyup, or ng-change. These directives allow you to specify a function to be executed when the event occurs. This function is defined in your controller and can perform any action you need.
How can I create animations in my AngularJS application?
AngularJS provides the ngAnimate module for creating animations. This module provides a way to create CSS-based animations and JavaScript-based animations. To create an animation, you define a CSS class or a JavaScript function that describes the animation. Then, you use directives such as ng-enter, ng-leave, or ng-move to apply the animation to elements in your view.
How can I integrate Bootstrap with AngularJS?
You can integrate Bootstrap with AngularJS by using the UI Bootstrap library. This library provides a set of AngularJS directives based on Bootstrap’s markup and CSS. It allows you to use Bootstrap components such as modals, tooltips, or datepickers in your AngularJS application in an AngularJS-friendly way.
How can I use the Bootstrap grid system in my AngularJS application?
The Bootstrap grid system is a flexible layout system that allows you to create responsive layouts. You can use it in your AngularJS application by using the appropriate Bootstrap classes in your views. For example, you can use the .row class to create a row, and the .col-* classes to create columns.
How can I test asynchronous code in my AngularJS application?
Testing asynchronous code can be challenging, but AngularJS provides a way to do it using the $timeout and $interval services. These services allow you to write tests that can wait for asynchronous operations to complete. You can also use the done function provided by Jasmine to signal that an asynchronous test has completed.
How can I debug my AngularJS application?
Debugging is an essential part of the development process. AngularJS provides several tools for debugging, such as the $log service for logging and the Batarang extension for Chrome for inspecting your application. You can also use the browser’s developer tools to inspect the DOM, view console messages, or step through your code.
How can I optimize the performance of my AngularJS application?
There are several ways to optimize the performance of your AngularJS application. Some of these include minimizing the number of watchers, using the one-time binding syntax to reduce the number of bindings, using the track by clause in ng-repeat to improve rendering performance, and using tools like ng-annotate to minify your code.
Rabi Kiran (a.k.a. Ravi Kiran) is a developer working on Microsoft Technologies at Hyderabad. These days, he is spending his time on JavaScript frameworks like Angular JS, latest updates to JavaScript in ES6 and ES7, Web Components, Node.js and also on several Microsoft technologies including ASP.NET 5, SignalR and C#. He is an active blogger, an author at SitePoint and at DotNetCurry. He is rewarded with Microsoft MVP (ASP.NET/IIS) and DZone MVB awards for his contribution to the community.