Learning Test-Driven Development (TDD) can be a great way to improve your skills as a professional software developer or, dare I say, software craftsman. There is a lot to learn about TDD as a process, and taking up the practice can be challenging at first. Once you master the basics, you sometimes just run into small technical obstacles. You know what you want to do, you just need to know how to do it. In this article, I’ll discuss one such specific obstacle that I recently had to overcome myself: how to test-drive the development of an AngularJS component?

Components were introduced in Angular 1.5 in order to make the transition to Angular 2 easier and to promote the component-based architectural style. I find components to be a clean and intuitive way to develop complex application in AngularJS, and TDD to be a very helpful practice, so naturally I want to be able to test-drive the development of components!

When writing a test for an AngularJS component, there are several challenges to solve: how to make the component available in your unit tests? How to get access to the controller that is used by the component? How to spy on dependencies of this component controller? And how to deal with promises that these dependencies might return?

There are a lot of different ways to solve this. As AngularJS evolves, testing approaches evolve with it. Apart from that, there are usually ten different ways to accomplish the same thing in AngularJS anyway, so finding one that works for you can be challenging. With that in mind, this article shows you just one possible approach that works for me.

In order to illustrate this approach, we’ll be creating a component for a simple calculator app. To make things a bit more challenging, the calculator will use an Angular service that performs the actual calculation, and return the result as a promise. In a real application, a component might also deal with services that fetch data from an API, or perform long-running operations that return promises.

Getting Started

You can follow along with this article by downloading the code from github. Simply clone the repository from https://github.com/daanstolp/tddcomponent/ and you’re good to go. If you run npm install, all required dependencies will be installed. A basic Karma configuration file is included, which sets up the testing. Tests can be run using npm run test. 1

If you want to skip ahead, I have uploaded the final solution to the complete-solution branch of the same repo.

Creating the Component Test-First

Let’s get started! The application we will be building simply adds two numbers and displays the result. The UI could be something like this:

Calculator

The basic plumbing is already there. We have an app called tddcomponent. We also have a calculatorService that performs the calculation. This is what the calculation service looks like:

(function() {
    'use strict';

    angular.module('tddcomponent')
           .service('calculatorService', calculatorService);

    function calculatorService($q) {
        this.add = function(n1, n2) {
            return $q.when(n1 + n2);
        };
    }
})();

As you can see, all it does is expose a function that adds two numbers, and returns the result as a promise using the $q library. Let’s start creating the component that will consume this service and make it available for use in our application.

The first thing I like to do is create a test that simply asserts that the component exists. I will probably throw away this test when I’m done, but it shows me that I have the basic components in place and that I can wire up everything correctly. Create a file called calculatorComponent.spec.js in the /js/specs folder and write the following test:

 1describe('Calculator component', function() {
 2
 3    var controller;
 4
 5    beforeEach(module('tddcomponent'));
 6
 7    beforeEach(inject(function($componentController) {
 8        controller = $componentController('calculator', {}, null);
 9    }));
10
11    it('should exist', function() {
12        expect(controller).toBeDefined();
13    });
14});

The interesting bits are on line 8: the use of the $componentController service. This service is part of angular-mocks and allows us to obtain the Controller that is associated with the component that we want to test. In this case, we are asking for the controller of our yet-to-be-created calculator component. We are not injecting any dependencies into the controller just yet (the second {} argument), and we do not set any bindings on the component (the third null argument).

This test should fail with a message that states that Angular has no idea what this calculator thing is supposed to be. So, let’s now create our basic component in a file called calculatorComponent.js in the /js/app folder:

 1(function () {
 2
 3    'use strict';
 4
 5    angular.module('tddcomponent')
 6           .component('calculator', {
 7                bindings: {}
 8           });
 9
10}());

Implementing Controller Logic for the Component

Next, let’s actually perform a calculation. We will design the component controller such that it has two properties that hold the numbers. It also exposes a function that will be called when the user clicks the Calculate button. Remember however, that we want to make use of the calculatorService to perform the calculation! So our component will only have two simple responsibilities:

  1. Call the calculatorService with the right parameters.
  2. Display the result.

In our tests, we want to verify that the controller fulfills these responsibilities. The calculatorService itself already has its own tests that verify that the calculation is performed correctly, so we do not need to test that explicitly.

We start with a test that asserts that the calculatorService is called when the performCalculation function is executed. The way to do this is by using a spy. A spy can monitor external dependencies, such as our calculatorService, and keeps track of the interactions with it. To verify if our controller has called the service correctly, we only have to ask our spy. So first we need to set up this spy in our test initialization code. Then in our test, we execute the performCalculation function and ask the spy if the service has been called with the expected parameters. Our test now looks like this:

 1describe('Calculator component', function() {
 2
 3    var controller;
 4    var calculatorService;
 5
 6    beforeEach(module('tddcomponent'));
 7
 8    beforeEach(inject(function($componentController, _calculatorService_) {
 9        calculatorService = _calculatorService_;    
10        spyOn(calculatorService, 'add')
11
12        controller = $componentController('calculator', 
13            { calculatorService: calculatorService }, null);
14    }));
15
16    it('should exist', function() {
17        expect(controller).toBeDefined();
18    });
19
20    it('should perform the calculation', function() {
21        controller.n1 = 2;
22        controller.n2 = 3;
23        controller.performCalculation();
24
25        expect(calculatorService.add).toHaveBeenCalledWith(2, 3);
26    });
27
28});

Notice how we are injecting the calculatorService on line 8, spying on it on line 10, and passing it to our controller on line 13. Our new test sets two numeric inputs, tells the controller to perform the calculation, and checks our spy to see if the calculatorService has been called correctly.

Since we have not implemented any controller code yet, much less the performCalculation function, this will result in a test error stating that performCalculation is not a function. So, let’s create it:

 5angular.module('tddcomponent')
 6       .component('calculator', {
 7           bindings: {},
 8           controller: function(calculatorService) {
 9               var ref = this;
10               
11               ref.performCalculation = performCalculation;
12
13               function performCalculation() {
14               }
15           }
16       });

When we run our test again, it fails for the right reason: our component never calls the calculatorService’s add function. So, let’s fix that by actually calling the calculatorService:

 5angular.module('tddcomponent')
 6       .component('calculator', {
 7           bindings: {},
 8           controller: function(calculatorService) {
 9               var ref = this;
10
11               ref.n1 = 0;
12               ref.n2 = 0;
13               
14               ref.performCalculation = performCalculation;
15
16               function performCalculation() {
17                   calculatorService.add(ref.n1, ref.n2);
18               }
19           }
20       });

The tests are green! At this point, we have verified that our component correctly tells the calculatorService to perform it’s calculation.

Verifying Results From a Promise

The component now performs the calculation, but we have completely ignored the result! Let’s change that, and have the component display the result. We start with a test:

28it('should show the result of a calculation', function() {
29    controller.n1 = 2;
30    controller.n2 = 3;
31    controller.performCalculation();
32
33    expect(controller.result).toBe(5);
34});

This will result in a test error, letting you know that result is undefined. So let’s create a placeholder for our result, so at least our test will recognize the property:

 5angular.module('tddcomponent')
 6       .component('calculator', {
 7           bindings: {},
 8           controller: function(calculatorService) {
 9               var ref = this;
10   
11               ref.n1 = 0;
12               ref.n2 = 0;
13               ref.result = 0;
14               
15               ref.performCalculation = performCalculation;
16   
17               function performCalculation() {
18                   calculatorService.add(ref.n1, ref.n2);
19               }
20           }
21       });

Our test still fails, but now with the message that it expected 0 to be 5. To fix this, we need to change our component controller such that it actually stores the result of the calculation. Let’s make this change now. First, we make a small change to our test setup, because we want our spy to call through to the real calculatorService. This makes sure that we have a real result to work with.

 8beforeEach(inject(function($componentController, _calculatorService_) {
 9    calculatorService = _calculatorService_;    
10    spyOn(calculatorService, 'add').and.callThrough();
11
12    controller = $componentController('calculator', 
13        { calculatorService: calculatorService }, null);
14}));

Notice the callThrough() call on line 10. Next, we need to update the controller to store the result. Since the service returns a promise, we need to use the then method and create a function that gets executed when the promise resolves. This function simply stores the result in the result property.

17function performCalculation() {
18    calculatorService.add(this.n1, this.n2)
19        .then(function(sum) {
20            ref.result = sum;
21        });
22}

And that’s it. We are now calling the service, and storing the result when the promise resolves. We’re done, right?! How come our test still shows the error that it expected 0 to be 5? The reason has to do with the fact that we are dealing with promises. Our test tries to verify the result, while the promise has yet to be resolved and processed by Angular. Fortunately, there is a simple way to fix this. In our test, between the call to performCalculation and the assertion, we need to call $apply() on the scope. Without going into too much detail, this basically tells angular to resolve all promises and perform all of its processing and data binding magic. After this call, our result will be available on the controller.

To implement this, we need a reference to the controller’s scope. Let’s set this up:

 1describe('Calculator component', function() {
 2
 3    var controller;
 4    var calculatorService;
 5    var scope;
 6
 7    beforeEach(module('tddcomponent'));
 8
 9    beforeEach(inject(function($componentController, _calculatorService_, $rootScope) {
10        calculatorService = _calculatorService_;
11        scope = $rootScope.$new();
12        spyOn(calculatorService, 'add').and.callThrough();
13
14        controller = $componentController('calculator', 
15            {
16                calculatorService: calculatorService,
17                $scope: scope
18            }, null);
19    }));

Notice how we create a new scope from the rootScope on line 11, and pass this scope to our controller on line 17.2 Finally, we update our test to call the $apply function:

1it('should show the result of a calculation', function() {
2    controller.n1 = 2;
3    controller.n2 = 3;
4    controller.performCalculation();
5
6    scope.$apply();
7
8    expect(controller.result).toBe(5);
9});

And that’s it, all tests are green!

In the complete-solution branch of the repository, I have created a template for this component, so that it actually has a UI. This branch contains the full solution and implements a simple demo page that shows the application in action.

The approach in this article shows one of the ways in which you can test-drive the creation of an AngularJS component. One of the difficulties with testing Angular code is getting access to the controller logic of a component or directive. The $componentController service makes this very easy to do. In addition, promises can sometimes be difficult to deal with in unit tests. Calling $apply() on the component’s scope causes all promises to be resolved, which allows you to verify your results in the unit tests. None of this is rocket surgery exactly, but when you are new to TDD, and you are trying to implement an AngularJS component, it can be very helpful to have these techniques in your toolkit.

Do you use a different approach to test AngularJS components? What other issues do you run into when trying to test-drive the development of your AngularJS app?


  1. Configuring npm and Karma can be the topic of entire blog posts of their own, so I won’t be discussing that here. From here on, I assume you know how to install the required packages and are able to run the tests. ↩︎

  2. Alternatively, we could simply pass the rootScope to the component and call $apply() on the rootScope. For a discussion about these two approaches, see http://stackoverflow.com/q/23756715↩︎