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.

Dave Baskin

View Comments

  • What if I don’t have the resize stuff you put in the controller but nonetheless want to test changing window widths (say for different media query states for example)? What I’m basically trying to say: How to integrate your controller code directly in the test?
    Thanks for any input!

Recent Posts

8-Step AWS to Microsoft Azure Migration Strategy

Microsoft Azure and Amazon Web Services (AWS) are two of the most popular cloud platforms.…

3 days ago

How to Navigate Azure Governance

 Cloud management is difficult to do manually, especially if you work with multiple cloud…

1 week ago

Why Azure’s Scalability is Your Key to Business Growth & Efficiency

Azure’s scalable infrastructure is often cited as one of the primary reasons why it's the…

3 weeks ago

Unlocking the Power of AI in your Software Development Life Cycle (SDLC)

https://www.youtube.com/watch?v=wDzCN0d8SeA Watch our "Unlocking the Power of AI in your Software Development Life Cycle (SDLC)"…

1 month ago

The Role of FinOps in Accelerating Business Innovation

FinOps is a strategic approach to managing cloud costs. It combines financial management best practices…

1 month ago

Azure Kubernetes Security Best Practices

Using Kubernetes with Azure combines the power of Kubernetes container orchestration and the cloud capabilities…

2 months ago