Angular TDD – Creating a directive

In this blogpost, I will explain how to create an Angular directive using TDD and show how it affects the code coverage report.

Prerequisites

This blogpost assumes the following has been installed on your machine.

  • Visual Studio Code (http://bit.ly/1KvI3PL)
  • Node.js (http://bit.ly/1Wdc3FQ)
  • A project with the following libraries (see this post on how to enable this):
    • Gulp (Task runner)
    • Bower (web package manager)
    • ngMock (AngularJS module relating to unit testing)
    • BardJS (library to avoid certain repetitive testing code)
    • Sinon 1.x or 2.x (Needed by BardJS to mock services)
    • AngularJS (MVW JavaScript framework)
    • Chai (TDD/BDD assertion library)
    • Mocha (JavaScript test framework)
    • Karma (JavaScript test runner by the AngularJS team)

Result

The following will be in place at the end of this post:

  • Directive unit tests
  • A directive
  • A code coverage report

This post is mainly a copy of the previous blogpost, since components and directives are basically twins that share a lot of functionality. There are some differences that will become apparent in the code coverage section. So, if you already know the basics, head over to the code coverage section.

Creating the unit test

Let’s start by writing a unit test for our directive. The directive is a copy of the component that we created in the previous post and will display the game character data on the screen.

Let’s create the test:

describe('game character directive', function () {
    var element, scope;
    var gameCharacterStub =
        {
            "id": 9460,
            "name": "Janus",
            "created_at": 1472328002062,
            "updated_at": 1472328002125,
            "slug": "janus",
            "url": "https://www.igdb.com/characters/janus",
            "people": [
                80788
            ],
            "games": [
                9498
            ]
        };

    beforeEach(function () {
        module('gameApp');
        module('partials');
        bard.inject(this, '$compile', '$rootScope');
    });

    beforeEach(function () {
        scope = $rootScope.$new();
        scope.model = gameCharacterStub;
        element = angular.element('<game-character-directive' +
            ' data-model="model"' +
            '>' +
            '</game-character-directive>'
        );
        //Compile a piece of HTML containing the directive
        $compile(element)(scope);
        //Fire all the watches, so the scope will be evaluated
        scope.$digest();
    });

    it('should render game character data', function () {
        var nameElement = angular.element(element[0].querySelector('div.name'));
        expect(nameElement.text()).to.equal('Janus');
    });
});

There are no functional differences between this directive and the component defined in the previous post.

This directive contains a div and returns the name of the game character. The first beforeEach has a module ‘partials’ that is needed for the templateUrl property. The second beforeEach contains the code to render the directive.

  1. The scope is initialized and populated.
  2. The HTML is defined using the angular.element syntax
  3. The HTML is built using the $compile statement
  4. The scope is evaluated using the $digest statement. This means that any HTML fields referring to the scope will be populated with the objects in the scope.

The unit test uses the querySelector syntax to select the div with the class name ‘name’. element.find, which uses JQLite, can be used only when we want to search an element by id. See https://docs.angularjs.org/api/ng/function/angular.element for a full list of supported functionality.

After we run the test, the following error occurs:

This is the result of the directive being nonexistent.

Creating the directive

Let’s create the directive:

(function () {
    'use strict';

    angular
        .module('gameApp')
        .directive('gameCharacterDirective', GameCharacterDirective);

    function GameCharacterDirective() {
        var directive = {
            bindToController: true,
            controller: 'GameCharacterController',
            controllerAs: 'gameCharacterVm',
            link: link,
            restrict: 'E',
            scope: {
                model: '='
            },
            templateUrl: 'src/directives/gameCharacterDirective.html'
        };
        return directive;

        function link(scope, element, attrs) {
            //Add event listener for 'click' event
            element.on('click', function (event) {
                console.log('hello');
            });
        }
    }
})();

And the gameCharacterDirective.html:

<div class="name">{{gameCharacterVm.model.name}}</div>

We use the controllerAs syntax as is explained in the Angular 1 style guide.

It is also possible to define controllerAs: ‘$ctrl’ and using only one html for both the component and the directive.

The directive contains a link function that writes hello to the console when a button is clicked. Normally we would have written a unit test for this event, but it will be deferred to the code coverage section. This is done to demonstrate the added effort to get full code coverage.

When we run Karma, the component unit test passes.

Code coverage

The code coverage report looks as follows:

The link function is executed after the directive is compiled. When a user clicks on the element, “hello” should be logged to the console. Let’s wrap up and expand the unit test to handle the click event.

it('should log "hello" to the console when a user clicks on the element', function () {
        console.log = sinon.spy(console, 'log');
        element.triggerHandler('click');
        expect(console.log).to.have.been.calledWith('hello');
    });

And the code coverage looks good:

[BLOCKQUOTE]

When testing with console.log we need to create a Sinon spy, because there is no way to read a logged string to the console with JavaScript.

Summary

We have created a unit test for a directive using the $compile functionality. We added a click event handler to the element in the directive and demonstrated that this can be covered with a unit test.

Leave a Reply