Angular TDD – Creating a controller

In this blogpost, I will explain how to create an Angular controller 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:

  • Controller unit tests
  • A controller
  • A code coverage report

 

I switched to Visual Studio Code and added the Karma Mocha reporter for easier test troubleshooting.

 

Creating the unit test

Before creating the controller, we will add the unit tests. The controller will be responsible for retrieving data from the game service and store this in a scope variable so it can be accessed by the view.

Let’s create the test:

 

describe('game character controller', function () {
    var gameCharacterController;

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

    beforeEach(function () {
        gameCharacterController = $controller('GameCharacterController');
    })

    it('should contain character data', function () {
        var characterData = gameCharacterController.data;
        expect(characterData).to.not.be.undefined;
    })
});

In the second beforeEach function, the controller is instantiated and the test specifies that the controller should have a property “data” that is defined once the instantiation is completed.

After we run the test, the following error occurs:

The error is rather vague in that there is no explicit mentioning of a controller that is missing.

 

Creating the controller

Let’s create the controller:

 

(function() {
    'use strict';

    angular
        .module('gameApp')
        .controller('GameCharacterController', GameCharacterController);

    function GameCharacterController() {
        var vm = this;
    }
})();

 

Check out the Angular JavaScript snippets extension by John Papa if you use VSCode, it will save you time when creating Angular objects.

The start is a very simple controller without any dependencies, properties or functions. Let’s see what Karma tells us after we save this file.

The error is gone, but now all tests are skipped, because the gameApp module wouldn’t exist. This is because in the previous post we created the service, which set the module gameApp.

 

angular
        .module('gameApp', [])
        .service('GameDatabaseService', GameDatabaseService);

This is because when Karma starts, the controller gets loaded before the service at which time the module is not set. Let’s fix this by removing the brackets in the module in the service and creating a module setter.

 

(function() {
    'use strict';

    angular.module('gameApp', [
    ]);
})();

Finally, we need to ensure that the module file gets loaded before the other js files. This can be done in the karma.conf.js file:

 

// list of files / patterns to load in the browser
        files: [
            'bower_components/angular/angular.min.js',
            'bower_components/angular-mocks/angular-mocks.js',
            'bower_components/bardjs/dist/bard.js',
            'src/**/*.module.js',
            'src/**/*.js',
            'test/**/*.spec.js'
        ],

After a reset of the Karma server:

This is what we expect, since the functionality has not been written yet.

 

Writing controller functionality

Let’s write the code to set the data of the controller:

 

(function () {
    'use strict';

    angular
        .module('gameApp')
        .controller('GameCharacterController', GameCharacterController);

    GameCharacterController.$inject = ['GameDatabaseService', '$q']
    function GameCharacterController(GameDatabaseService, $q) {
        var vm = this;

        setCharacterData();

        function setCharacterData() {
            GameDatabaseService.retrieveData().then(function (data) {
                vm.data = data;
            }, function (error) {
                console.log(error);
            })
        }
    }
})();

We need to inject the service and $q dependencies into the controller, because the service returns the data as a promise.

Performing true TDD (as in: finish tests before starting functionality) grows in difficulty as your project grows in complexity. It is often the case that you start writing a unit test, start writing a controller, rewrite the unit test, rewrite controller and repeat the last two steps until you are satisfied about the result. I believe these are healthy iterations if you are doing this with a test-driven mindset.

We need to change our unit test to wait for a promise to complete before checking the data variable. We need to consider carefully what we are testing. We only want to test controller logic and not service logic. What we are going to do is to mock the service and return a fake data object. The resulting spec file will look as follows:

 

describe('game character controller', function () {
    var gameCharacterController;
    var gameDatabaseServiceFakeResponse = [
        {
            "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');
        bard.inject(this, '$controller', '$q', 'GameDatabaseService', '$rootScope');
    })

    beforeEach(function () {
        bard.mockService(GameDatabaseService, {
            retrieveData: $q.when({
                data: gameDatabaseServiceFakeResponse
            }),
            _default: []
        });
    })

    beforeEach(function () {
        gameCharacterController = $controller('GameCharacterController', {
            GameDatabaseService: GameDatabaseService,
            $q: $q
        });
    })

    it('should contain character data', function () {
        expect(gameCharacterController.data).to.be.undefined;
        $rootScope.$apply();
        expect(gameCharacterController.data).to.not.be.undefined;
    })
});

At the top a fake JSON object is defined that represents the data. The bard.inject statement is enriched with the service to be mocked. This mocked service return the fake object in a promise (as the real service would do).
As soon as the $rootScope.$apply statement is executed, the promise is resolved and the controller data is populated.

 

Make sure you have either Sinon v1 or v2 installed, since BardJS is not compatible with the latest version of sinon (at the time of writing v3). If you would have Sinon 3.x installed in your npm folder, BardJS (which defaults to node_modules for sinon) will throw a stub error.

 

Code coverage

The code coverage report looks as follows:

Let’s add the error handling to the unit test (both tests are displayed for completeness):

 

it('should contain character data', function () {
        //Each test needs its separate instance of a controller for the sinon.stub to work.
        gameCharacterController = $controller('GameCharacterController', {
            GameDatabaseService: GameDatabaseService,
            $q: $q
        });

        expect(gameCharacterController.data).to.be.undefined;
        $rootScope.$apply();
        expect(gameCharacterController.data).to.not.be.undefined;
    })

    it('should return an error, when the service fails to return data', function () {
        GameDatabaseService.retrieveData.restore();
        var error = new Error('Whoops, cannot retrieve character data');

        sinon.stub(GameDatabaseService, 'retrieveData').returns($q.reject(error));

        //Each test needs its separate instance of a controller for the sinon.stub to work.
        gameCharacterController = $controller('GameCharacterController', {
            GameDatabaseService: GameDatabaseService,
            $q: $q
        });

        expect(gameCharacterController.error).to.be.undefined;
        $rootScope.$apply();
        expect(gameCharacterController.error).to.not.be.undefined;
    })

 

The restore functionality at the start of the second test allows us to redefine the retrieveData method by using sinon.stub. Sinon.stub cannot be combined with the controller instance in the beforeEach, because it needs to run before the instantiation of the controller.

Let’s open the code coverage report:

Summary

We have created a unit test for a controller using the mockService functionality of bardJS. Next, we created the controller and added a unit test to complete our code coverage report.

In the next post, we will do TDD for an Angular component.

Leave a Reply