Angular.js – Testing Directives Reply

Rick HerrmannOne of the features of Angular is that it was built with testability in mind.  The separation of the DOM layer, controllers, and services, as well as the use of dependency injection make it relatively easy to develop testable code.

I am very familiar with the practice of writing unit tests and have been testing C# code for several years.  Although I did find testing in Angular to be a little confusing at first, once I got used to the code required to set-up the tests – it was pretty easy.

Our applications contain a mix of controllers, views, routes, services, filters, and directives. Today I am going to show how you can unit test your directives.

(For the examples, I am using the mocha testing framework)

The goal of an Angular directive is to isolate a specific feature of an application.  Code that does one thing is, in general, easier to test than code that does many things.  Because of this, directives lend themselves well to be tested.

Here is the directive we are testing.  It uses a service to get a list of links, and then display the links in a sub-navigation menu.  The behavior is encapsulated in a directive so it can be used on multiple pages:

'use strict';
(function() {

	angular
		.module('app.directives')
		.directive('subNav', Directive);

	function Directive() {
		var vm;

		return {
			link: link,
			controller: controller,
			controllerAs: 'vm',
			bindToController: true,
			templateUrl: 'app/directives/sub-nav/sub-nav.html',
			restrict: 'E',
			scope: {
				category: '@category'
			}
		};

		function link(scope, element, attrs) {
		}

		/* @ngInject */
		function controller(SubNavigationService) {
			/*jshint validthis: true */
			vm = this;
			vm.subNavLinks = [];
			activate();

			function activate() {
				SubNavigationService.getSubNavItems(vm.category)
					.then(function(data) {
						if(data.length > 1) {
							vm.subNavLinks = data;
							vm.showSubNav = true;
						}
					});
				}
			}
		}
	}
})();

 

Setting up the spec

The first step in writing any Angular test is to create a spec file.  I follow the convention of directive-name.directive.spec.js.  As part of the test setup, we need to include our app module.  This is done using the module function from the angular-mocks library (line 9 below).

Our directive has one dependency being injected (the SubNavigationService).  Angular handles the dependency injection automatically when the app is running, but for our test we need to use the inject() function from angular-mocks to pass in the service.(see line 11 below).  Using “_” before and after the dependency is a convention – the underscores are ignored by the injector.

The other items being injected on line 11 are needed to setup the test.  Going through the rest of the beforeEach function:

  • $rootScope is used to generate a scope for the directive (line 13)
  • $q is used to create the promise for the mocked service (lines 15 and 16)
  • sinon is used to stub the call to the SubNavigationService.getSubNavItems function (line 18)
  • The html to reference the directive is used to create an angular element (line 19)
  • $compile is used compile the directive and bind the scope (line 20)
  • We use the element created in line 17 to get a reference to the directive’s controller. (line 22)

Each block will run before every individual spec in the file, so we are guaranteed to have the same state for every spec.

 

'use strict';
describe('sub-nav specs', function() {
	var SubNavigationService;
	var scope;
	var element;
	var controller;
	var subNavigationPromise, subNavigationDeferred;

	beforeEach(module('app'));

	beforeEach(inject(function(_SubNavigationService_, $rootScope, $compile, $q) {
		SubNavigationService = _SubNavigationService_;
		scope = $rootScope.$new();

		subNavigationDeferred = $q.defer();
		subNavigationPromise = subNavigationDeferred.promise;

		sinon.stub(SubNavigationService, 'getSubNavItems').returns(subNavigationPromise);
		element = angular.element(<sub-nav category="category"></sub-nav>');
		$compile(element)(scope);
		$rootScope.$digest();
		controller = element.controller('subNav');
}));

Since this directive has very targeted functionality , there are only three scenarios I need to setup – when the service returns:

  1. More than one navigation item
  2. One navigation item
  3. No navigation items

 

Scenario 1

We create a new describe block for the scenario on line 26.  In addition to the beforeEach functions above, each nested describe block can also have a beforeEach function (line 27).  The beforeEach function is where we can setup the spec to match the scenario: in this case, having the SubNavigationService return more than 1 item.  On line 28, we resolve the mocked service call with 5 fake sub nav items.

Pro Tip: to get a promise to resolve on a stubbed function you have to call scope.$apply().  This is an easy mistake to make and tricky to debug because you don’t get an error message – you just don’t get any data.

On lines 31 and 34 we setup the it functions to verify the expectations.  This is where we can use the reference to the controller from line 22 (above) to access the scope variables.  There are a few things we can check to see if our code behaves correctly in this scenario:

  1. The list of links from the service is saved in the subNavLinks array on the controller (line 32)
  2. The showSubNav flag on the controller should be true (line 35)
  3. Using the reference to the element (from line 19 above), we can use jQuery selector syntax to query the html that is created.  In this case we are verifying that there is a ul created with an li for each sub nav item.  I find this to be a much easier way to test the UI than writing end-to-end tests.

|
	describe('when there are 2 or more items for the category', function() {
		beforeEach(function() {
			subNavigationDeferred.resolve(getFakeSubNavItems(5));
			scope.$apply();
		});
		it('then there should be a link for each item', function() {
			expect(controller.subNavLinks.length).to.be.equal(5);
		});
		it('then the links should be displayed', function() {
			expect(controller.showSubNav).to.be.true;
			expect(element.find('li'.length).to.be.equal(5);
		});
	});

 

Scenarios 2 and 3

Scenarios 2 and 3 look a lot like scenario 1.  The difference is in the setup on lines 42 and 54, where instead of returning 5 items from the service, we return 1 and 0.  The expect statements are verifying the behavior specific to having one or zero links.

	describe('when there is 1 item for the category', function() {
		beforeEach(function() {
			subNavigationDeferred.resolve(getFakeSubNavItems(1));
			scope.$apply();
		});
		it('then the links should not be displayed', function() {
			expect(controller.subNavLinks.length).to.be.equal(0);
			expect(controller.showSubNav).to.be.false;
			expect(element.find('li').length).to.be.equal(0);
		});
	});
	describe('when there are 0 items for the category', function() {
		beforeEach(function() {
			subNavigationDeferred.resolve([]);
			scope.$apply();
		});
		it('then the links should not be displayed', function() {
			expect(controller.subNavLinks.length).to.be.equal(0);
			expect(controller.showSubNav).to.be.false;
			expect(element.find('li').length).to.be.equal(0);
		});
	});

 

Conclusion

Unit tests in Angular can be a little hard to get started with, but once you get used to the required setup code they are actually pretty easy to write.  In future blog posts we will look at testing the other Angular components (controllers, routes, services, and filters).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s