Skip to content
  • Services
  • About
  • Partners
  • Work
  • Insights
  • Careers
  • Contact
  • Services
  • About
  • Partners
  • Work
  • Insights
  • Careers
  • Contact
July 16, 2014

Unit Testing w/ AngularJS

Web and Mobile Development

Oh how far we’ve come in the web development world. Cross-browser support and performance, once touted as core features, are now table stakes. Meanwhile, automated tests have become pretty much standard in today’s JavaScript-driven applications. So naturally, we are beginning to expect frameworks to optimize around separation of concerns and testability. AngularJS came on the scene with its 1.0 release in 2012 and was “designed from the ground up to be testable.”

Or so they say.

Here we’ll examine just how easy it is to unit test AngularJS front-ends and what that can mean for today’s applications.

Dependency Injection

Unit testing is all about isolation and dependency injection makes isolation a breeze. Seasoned Java developers have known this for years. The Spring framework, ubiquitous in Java backends today, introduced dependency injection (or at least popularized it) and brought a great number of benefits in terms of decoupling, modularity, flexibility and testability. Simply put, dependency injection lets you swap out real implementations for mock ones and it damn near forces you to decouple every component.

Karma Overview

Karma is a test runner designed to work with AngularJS. It is agnostic of test frameworks and allows teams to easily execute tests in real browsers and headlessly. It integrates easily into continuous integration systems and build tools. Karma is incredibly easy to work with so rolling your own config should be straightforward enough. However, using a standard project template like ngBoilerplate will make things even easier.

Working with Jasmine

While there are several options for unit testing Angular, Jasmine is default. Jasmine’s syntax is simple and semantic and well, a little silly at times. The structure of a unit test in Jasmine looks like this:

describe('my Module') {
    var variables; //Expose items constructed in beforeEach()

    beforeEach() {} //Any setup that needs to happen for each test

    afterEach() {} //Any cleanup needed for each test

    it() {
        //Setup, Act, Expect
    }

    it() { } //Additional tests
}

Getting familiar with Jasmine’s matchers and spies is really the only involved task. Fortunately, fitting Jasmine to AngularJS is incredibly easy thanks to dependency injection.

var controller, scope;

beforeEach(module('myModule'));

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

ngMock

The ngMock module is injected during tests and replaces some core components of the framework to better accommodate the needs of unit tests.

$timeout

As tests are part of the development workflow, tests must execute extremely quickly. Angular solves this by forcing all $timeout calls to execute immediately after the current thread completes, e.g. setTimeout(myFn, 0); In most cases, using timeouts is frowned upon, but there are obviously many cases where they’re strictly necessary. It’s important to keep in mind that code cannot rely on specific timing to work properly within unit tests.

$httpBackend

In order to isolate functionality and improve execution time, unit tests should never call out for data. The $httpBackend service automatically replaces $http (Angular’s Ajax service) so all such calls can be mocked.

There are two key concepts to employ with $httpBackend: expect and when. Simply put expect() tells your test to expect that a call is made and made correctly. On the other hand, when() allows you to inject a mock response so that you can test that the data is used properly. Both of these are absolutely critical to effective unit testing of Angular apps.

One brilliant feature of $httpBackend is its flush() method. Rather than forcing asynchronous test code, flush() allows tests to explicitly “execute” pending requests. This allows you to avoid the usual array of promises, callbacks and closures in your test code.

Testing Controllers

Controllers tend to be the easiest to test. Here is a simple example of a Controller and its unit test:

angular.module('MyModule').controller('MyCtrl', function($scope) {
    $scope.value = 0;
    $scope.maxValue = 3;
    $scope.incrementValue = function() {
        if ($scope.value < $scope.maxValue) {
            $scope.value++;
        } else {
            $scope.value = 0;
        }
    };
});

describe('MyCtrl', function() {
    var scope, controller;

    beforeEach(inject(function($controller, $rootScope) {
         scope = $rootScope.$new();
         controller = $controller('MyCtrl', {
              $scope: scope
         });
    }));
    
    it('has correct initial values', function() {
        expect(scope.value).toBe(0);
        expect(scope.maxValue).toBe(3);  
    });
    
    it('increments correctly', function() {
        scope.incrementValue();
        expect(scope.value).toBe(1);
        scope.incrementValue();
        expect(scope.value).toBe(2);
        scope.incrementValue();
        expect(scope.value).toBe(3);
        scope.incrementValue();
        expect(scope.value).toBe(0);
    });
});

Let’s take our ridiculously trivial example and make it a little more complicated. Instead of hardcoding the initial values. Let’s get them via an XHR:

angular.module('MyModule').controller('MyCtrl', function($scope, $http) {
    $scope.incrementValue = function() {
        if ($scope.value < $scope.maxValue) {
            $scope.value++;
        } else {
            $scope.value = 0;
        }
    };

    $http.get('api/incrementor/config').success(function(data) {
        $scope.value = data.initialValue;
        $scope.maxValue = data.maxValue;
    });
});

describe('MyCtrl', function() {
    var scope, createController, httpBackend;

    beforeEach(inject(function($controller, $rootScope, $httpBackend) {
        scope = $rootScope.$new();
        httpBackend = $httpBackend;
        
        createController = function() {
            return $controller('MyCtrl', {
                $scope: scope,
                $http: $httpBackend
            });
        };
    }));
    
    it('sets correct initial values', function() {
         httpBackend.expectGET('api/incrementor/config').respond(200, {
            initialValue: 0,
            maxValue: 3
        });
        createController();
        httpBackend.flush();
        expect(scope.value).toBe(0);
        expect(scope.maxValue).toBe(3);
    });
    
    it('increments correctly', function() {
        scope.incrementValue();
        expect(scope.value).toBe(1);
        scope.incrementValue();
        expect(scope.value).toBe(2);
        scope.incrementValue();
        expect(scope.value).toBe(3);
        scope.incrementValue();
        expect(scope.value).toBe(0);
    });
});

Testing Directives

When writing directives there are a couple of things to consider:

  1. Use templateUrl for any non-trivial templates
  2. Business logic should go in the directive’s controller function
  3. All DOM manipulation belongs in the directive’s link function

When testing directives there are corresponding considerations:

  1. Use html2js preprocessor to more easily handle templateUrl directives
  2. The directive’s controller function can be tested similarly to a standalone controller
  3. DOM manipulation can (and should) be unit tested

Each directive test will look roughly like the following:

it('tests myDirective', function() {
    var element = $compile('')($rootScope);
    //test element to see what it contains
    //test functions and values on $rootScope like in a standalone controller
});

Testing Services and Filters

Testing Services and Filters should be straightforward after mastering controllers. The same principles apply.

Coverage Goals

Ask a random developer how important code coverage is, the answer will likely be directly proportional to how high his/her code coverage is. The truth is there is no universally right answer. For those new to unit testing, work to get the most complex bits of logic under test first. Once you have more fluency and have learned to write more easily testable code, expand coverage to whatever makes sense for your project.

Arguably the easiest and most valuable code to unit test will likely be the controllers. Start there and expand to filters, services and directives. Directives are listed last here because they can often be the most challenging. But keep in mind, directives are meant to be reusable, which mean bugs and regressions can be both more likely and more disastrous.

End-to-End Considerations

As we’ve covered, unit tests validate individual pieces of functionality. End-to-end tests are needed to make sure those modules work together properly. They are there to validate user stories. Modus will follow up with a full post on this topic.

Tips for Avoiding Headaches

  1. No DOM manipulation in controllers
  2. Separate controller and link code in directives
  3. Each function should have one clear purpose
  4. Don’t hide key functionality with closures
  5. Just go ahead and bookmark $httpBackend docs now
  6. Focus on quality over quantity when unit testing

Conclusions

AngularJS along with Jasmine and Karma do indeed make unit testing easy. Following the guidelines above should help lessen the learning curve and encourage more easily testable code.

Having strong unit tests behind your application provide peace of mind and are incredibly valuable once you’ve hit production. However, unit tests are not sufficient. E2E tests must be part of your workflow and not surprisingly, Angular has just the solution for that too.

Posted in Web and Mobile Development
Share this

Jay Garcia

Jay Garcia is co-founder and managing director at Modus Create. He is a U.S. Air Force veteran with 20 plus years of technology and consulting experience in leading RIA development for companies around the world. He is co-organizer of the NoVa.JS and NYC.JS meetups, and is actively involved in the software communities that provide business frameworks and technologies, which enable rich mobile and desktop web experiences.
Follow

Related Posts

  • AngularJS-Tricks-with-angular-extend
    AngularJS: Tricks with angular.extend()

    AngularJS has this built-in method for doing object to object copies called angular.extend(). It is…

  • Test your Angular apps with Protractor
    Testing AngularJS Apps with Protractor

    Do the words “automated” and “tests” make you cringe a little? Creating automated tests for…

Subscribe to the Modus Newsletter

Receive the latest insights from our team each month.

Let's Chat

If forms aren’t your thing, you can always call us (+1-855-721-7223).

Modus-Logo-Primary-White.svg
  • Services
  • About
    • Newsroom
  • Partners
  • Work
  • Insights
    • Blog
    • Modus Labs
  • Careers
Virginia (US)

12100 Sunset Hills Road
Suite 150
Reston, Virginia, 20190
Tel: +1-855-721-7223

California (US)
12130 Millennium Dr

Los Angeles, CA 90094

Missouri (US)
609 E High St

Jefferson City, MO 65101

Romania

Str. Mihai Veliciu, no. 17
Cluj-Napoca, Romania
Tel: +40-0786-887-444

Costa Rica

2nd Floor, Plaza Koros, Av 3
San José, Santa Ana, Costa Rica

© 2021 Modus. All Rights Reserved.

Privacy Policy | Accessibility Statement | Sitemap

Scroll To Top
This website uses cookies.
These cookies are used to collect information about how you interact with our website and allow us to remember you. We use this information in order to improve and customize your browsing experience, and for analytics and metrics about our visitors both on this website and other media. To find out more about the cookies we use, see our Privacy Policy.

Accept
Privacy & Cookies Policy

Privacy Overview

This website uses cookies to improve your experience while you navigate through the website. Out of these cookies, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may have an effect on your browsing experience.
Save & Accept