Unit Testing with AngularJS

Published Mar 01, 2017Last updated Mar 20, 2017
Unit Testing with AngularJS

In Angular, everything seems to have a steep learning curve. Unit testing an Angular app definitely doesn't escape this paradigm.

When I first started with TDD and Angular, I felt that I was spending twice (maybe more) as much time on figuring out just how to test and maybe even more on setting my tests up correctly. But as Ben Nadel put it in his blog, there are ups and downs in the Angular learning process - his graph (below) is definitely my experience with Angular.

Source: http://www.bennadel.com/blog/2439-my-experience-with-angularjs-the-super-heroic-javascript-mvw-framework.htm

Source: Ben Nadel

However, as I have progressed in learning more about Angular and unit testing, I am spending much less time on setting up tests and much more time on making tests go from red to green — which is a good feeling.

I have come across different methods of setting up my unit test to mock services and promises. So onto the code! I am sure you don't wanna listen to some guy blab about his love—err...accomplishments—learning a framework.

This is how I started out mocking my services and promises. I'll use a controller, but services and promises can be mocked in other places using the same technique.

describe('Controller: Products', function () {
    var//iable declarations
        $scope,
        $rootScope,
        ProductsMock = {
            getProducts: function () {
            } // There might be other methods as well but I'll stick to one for the sake of conciseness
        },
        PRODUCTS = [{},{},{}]
    ;

    beforeEach(function () {
        module('App.Controllers.Products');
    });

    beforeEach(inject(function ($controller, _$rootScope_) {
        //Set up our mocked promise
        var promise = { then: jasmine.createSpy() };

        //Set up our scope
        $rootScope = _$rootScope_;
        $scope = $rootScope.$new();

        //Set up our spies
        spyOn(ProductsMock, 'getProducts').andReturn(promise);
    
        //Initialize the controller
        $controller('ProductsController', {
            $scope: $scope,
            Products: ProductsMock
        });
    
        //Resolve the promise
        promise.then.mostRecentCall.args[0](PRODUCTS);
    
    }));
    
    describe('Controller Initialization', function () {
        it('should have a populated array of products', function () {
        	expect('ProductsMock.getProducts').toHaveBeenCalled();
        });
    });
});

This worked, but as time went on, I thought there must be a better way. For one, I hated the

promise.then.mostRecentCall

thing, and if I wanted to reinitialize the controller, then I had to pull it out of the beforeEach block, and inject it individually into each test, making our tests not DRY.

There has to be a better way...

Then I came across several other posts, blogs, and Stackoverflow examples (you name it, I was probably there), and I saw the use of the $q library.

D'oh!

Why set up a whole mock promise when we can just use the tool that Angular gives us. Our code looks cleaner and it's much more intuitive.

  • no ugly promise.then.mostRecent thing. Then to DRY everything out, wrap the controller instantiation in a function so we can have more control over how the controller behaves for setting up our different test conditions through the use of parameters and CONSTANTS. Now we're getting somewhere.

Next in the iteration of unit testing was this:

describe('Controller: Products', function () {
    var//iable declarations
        $scope,
        $rootScope,
        $q,
        $controller,
        products,
        PROMISE = {
            resolve: true,
            reject: false
        },
        PRODUCTS = [{},{},{}] //constant for the products that are returned by the service
    ;
    
    beforeEach(function () {
        module('App.Controllers.Products');
        module('App.Services.Products');
    });
    
    beforeEach(inject(function (_$controller_, _$rootScope_, _$q_, _products_) {
        $rootScope = _$rootScope_;
        $q = _$q_;
        $controller = _$controller_;
        products = _products_;
        $scope = $rootScope.$new();
    }));
    
    function setupController(product, resolve) {
        //Need a function so we can setup different instances of the controller
        var getProductsPromise = $q.defer();
    
        //Set up our spies
        spyOn(products, 'getProducts').andReturn(getProductsPromise.promise);
    
        //Initialise the controller
        $controller('ProductsController', {
            $scope: $scope,
           products: products
        });
    
        // Use $scope.$apply() to get the promise to resolve on nextTick().
        // Angular only resolves promises following a digest cycle,
        // so we manually fire one off to get the promise to resolve, or in this
        // case we can choose through the parameters being passed in whether or
        // not to resolve the promise - thus covering our test cases. 
        if(resolve) {
            $scope.$apply(function() {
                getProductsPromise.resolve(product);
            });
        } else {
            $scope.$apply(function() {
                getProductsPromise.reject();
            });
        }
    }
    
    describe('Resolving and Rejecting the Promise', function () {
        it('should return the first PRODUCT when the promise is resolved', function () {
            setupController(PRODUCTS[0], PROMISE.resolve); // Set up our controller to return the first product and resolve the promise. 
            expect('to return the first PRODUCT when the promise is resolved');
        });
    
        it('should return nothing when the promise is rejected', function () {
            setupController(PRODUCTS[0], PROMISE.reject); // Set up our controller to return first product, but not to resolve the promise. 
            expect('to return nothing when the promise is rejected');
        });
    });
});

This is starting to feel like the way it should be! We can mock what we need to mock, we can set our promise to resolve and reject.
We can truly test the two possible outcomes and this feels good.

If you're interested in learning more about AngularJS, check out this page for various kinds of resources to help you learn AngularJS!


This tutorial was originally posted by the author on his blog. This version has been edited for clarity and may appear different from the original post.

Discover and read more posts from Sten Muchow
get started
Enjoy this post?

Leave a like and comment for Sten