AngularJS/MVC Cookbook Unit Testing

I’ve updated the Simple Routing example in the AngularJS/MVC Cookbook with unit tests. Unit tests are meant to test a piece of an application independently from any dependencies it might have. AngularJS provides a dependency injection framework that allows (and encourages) unit testing pieces of a web application.

For this application, I am using Jasmine as the framework for implementing unit tests around the Javascript components. Tests, called “specs”, are written in a simple syntax that groups tests and defines the expectations of the test. For example:

describe('Basic setup test', function () {

    it('should expect true to be equal to true.', function () {
        expect(true).toBe(true);
    });
    
});

I’m interested in testing my controllers. For the “Home” controller, I want to ensure that the “name” property is just set to “World” (so I get a “Hello World” message on the page).

angular
    .module('myApp.ctrl.home', [])
    .controller('homeCtrl', ['$scope', function ($scope) {

        $scope.name = "World";

    }]);

AngularJS provides helper functionality that allows you to mock some of its infrastructure. If you create unit tests for .NET code, you of course need some of the .NET runtime in order to execute these tests. Similarly, you need some of the AngularJS framework in order to get modules and injection services in place.

Here is the code for testing the home controller:

describe('Home Controller', function() {

    var scope, controller;
    
    beforeEach(function() {
        module('myApp.ctrl.home');
    });

    beforeEach(inject(function ($controller, $rootScope) {
        scope = $rootScope.$new();
        controller = $controller("homeCtrl", {
            $scope: scope
        });
    }));

    it('should expect name to be World', function () {
        expect(scope.name).toBe("World");
    });
});

Before the test is run, I first need to create a mock Angular module (named ‘myApp.ctrl.home’), and then create my controller instance and pass in the new scope. Ultimately, I get an instance of my home controller that I can then test.

Of course, for the “Home” controller, the test is very simple. And the “Contact” controller follows the same pattern. The “About” controller is more interesting, however.

The “About” controller keeps track of the current window width and height and provides properties that the view displays. These values are updated as the browser window is resized. To obtain these values, the controller needs the browser’s window object. To effectively unit test the controller, however, you don’t want a dependency directly on this object. Therefore, AngularJS provides a $window service that abstracts the browser’s window object and that can be injected into your code.

angular
    .module('myApp.ctrl.about', [])
    .controller('aboutCtrl', ['$scope', '$window', function ($scope, $window) {

        var w = angular.element($window);

        $scope.version = "1.0.0";
        $scope.windowWidth = 0;
        $scope.windowHeight = 0;

        var setDimensions = function() {
            $scope.windowWidth = w.width();
            $scope.windowHeight = w.height();
        };

        w.bind('resize', function () {
            $scope.$apply(function () {
                setDimensions();
            });
        });
        setDimensions();

    }]);

Note that in this code the $window service must be wrapped in a angular.element wrapper (the equivalent to the jQuery wrapper “$(window)”) so that the width, height, and bind functions can be used.

One last very important thing to note is that the function handling the “resize” event can’t just simply set the windowWidth and windowHeight properties on the scope. AngularJS won’t know that these properties have changed and that the view needs to be updated. This is why the $scope.$apply method is used to wrap the updating of the width and height properties. (See AngularJS Concepts Runtime section for more information about this.)

In order to effectively unit test this controller we will need to mock these window-specific functions. Here’s the code:

describe('About Controller', function () {

    var scope, controller, mockWindow, resizeFxn;
    var currentWidth = 505, currentHeight = 404;

    beforeEach(function() {
        module('myApp.ctrl.about');
    });
    
    beforeEach(inject(function ($controller, $rootScope) {
        scope = $rootScope.$new();
        spyOn(angular, "element").andCallFake(function () {
            mockWindow = jasmine.createSpy('windowElement');
            mockWindow.width = jasmine.createSpy('width').andCallFake(function() {
                return currentWidth;
            });
            mockWindow.height = jasmine.createSpy('height').andCallFake(function () {
                return currentHeight;
            });
            mockWindow.bind = jasmine.createSpy('bind').andCallFake(function (evt, fxn) {
                resizeFxn = fxn;                
            });
            mockWindow.unbind = jasmine.createSpy('unbind');
            return mockWindow;
        });
        controller = $controller("aboutCtrl", {
            $scope: scope
        });
    }));

    it('should initially have expected window width and height', function () {
        expect(scope.windowWidth).toBe(505);
        expect(scope.windowHeight).toBe(404);
    });

    it('should have the expected version number', function () {
        expect(scope.version).toBe("1.0.0");
    });

    it('should bind to the window resize event', function () {
        expect(mockWindow.bind).toHaveBeenCalledWith("resize", jasmine.any(Function));
    });

    it('should update window width and height upon resize event', function () {
        expect(resizeFxn).not.toBeUndefined();
        currentWidth = 606;
        currentHeight = 303;
        resizeFxn();
        expect(scope.windowWidth).toBe(606);
        expect(scope.windowHeight).toBe(303);
    });
});

First we use a Jasmine feature, spyOn, to intercept the element method on the global angular object and return a fake object instead. This fake object consists of a number of “spy” objects for which we can control the results. In the end, this configuration allows us to execute specific tests on the controller without having to use the real browser window object.

Up next, running the Jasmine unit tests.

Stay Informed

Sign up for the latest blogs, events, and insights.

We deliver solutions that accelerate the value of Azure.
Ready to experience the full power of Microsoft Azure?

Atmosera is thrilled to announce that we have been named GitHub AI Partner of the Year.

X