Unit Testing




Aaron N. Smith, @aaron_n_smith

What is unit testing?

Yes really, let's cover this real quick.

Unit Testing Explained

(as quickly as possible)

  • Tests of individual "units" of code
  • Validate that various inputs produce expected outcomes
  • Proves that a function, module, etc. works as expected
  • Helps document valid conditions and assumptions
  • Unit tests are necessary but not sufficient
  • Unit tests prevent introduction of bugs

Unit Testing's Value


Those who think unit testing is a waste of time
probably haven't worked on a big project/team.

Upfront cost to prevent headaches down the road.

Why Angular?


"designed from the ground up to be testable"

Decent App Architecture

Good separation of concerns
Easy to break into discrete modules and units

Dependency Injection!!


DI  mock implementations  isolation


Dependency Injection != Service Locator Pattern

Karma

  • Fantastic test runner built by the Angular team
  • Test framework agnostic
  • Supports Jasmine, Mocha, QUnit
  • Run in one or many browsers (or headless)

ngMock Module

Built-in utilities to make unit testing easier and faster

Problem
Asynchronous code is hard to test
In other words: callback hell

Solution
ngMock allows core Angular services to 
be controlled in a synchronous fashion

Seed Projects

ngBoilerplate gets you started with...

  • Clean, scalable project organization
  • No need to write scripts or configuration to build app or execute tests
  • Just define your dependencies (you're using bower, right?) and you're off and running
  • Give it a try

Unit testing is valuable


AngularJS is testable


So let's dive in!



Step 1: Choose Test Framework

Jasmine, Mocha, QUnit  All Supported

Jasmine is Karma's default 
and conveniently
it's the only one I've worked with

So let's go with Jasmine...

Jasmine in 30 Seconds

  • Suites  describe()
  • Specs  it()
  • Setup/Teardown  beforeEach(), afterEach()
  • Matchers  expect(a).toBeEqual(b)
  • Spies  expect(fn).toHaveBeenCalled()

Example: Simplest Controller Ever

angular.module('MyModule').controller('MyCtrl', function($scope) {
  $scope.value = 0;
  $scope.maxValue = 2;

  $scope.incrementValue = function() {
    if ($scope.value < $scope.maxValue) {
      $scope.value++;
    } else {
      $scope.value = 0;
    }
  };
});


Example: Corresponding Unit Test

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

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

  it('has correct initial value', function() {
    expect(scope.value).toEqual(0); expect(scope.maxValue).toEqual(2);
  });

  it('increments correctly', function() {
    scope.incrementValue(); expect(scope.value).toEqual(1);
    scope.incrementValue(); expect(scope.value).toEqual(2);
    scope.incrementValue(); expect(scope.value).toEqual(0);
  });
});

Let's make it dynamic!

Example: Controller

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

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

Testing $http Calls


ngMock provides $httpBackend service to help

Usage
Inject $httpBackend in place of $http

Outcome
No XHRs in your unit tests  faster + isolated  good

$httpBackend Key Components


// Verify that a request is made and handled (properly)
expect().respond(...);

// Verify that a response is handled (properly) if called
when(...).respond(...);

// Force synchronous execution of pending request(s)
flush(); 

Other $httpBackend Gems


// Make sure all expect() calls were made
afterEach($httpBackend.verifyNoOutstandingExpectation);

// Make sure there are no calls waiting to be flushed
afterEach($httpBackend.verifyNoOutstandingRequest); 

// Use in complex, multi-step specs
$httpBackend.resetExpectations(); 

Example: Corresponding Test

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() {
    // Continued on next slide
  });
});

Example: Test (Continued)

it('sets correct initial values', function() {
  httpBackend.expectGET('api/incrementor/config').respond(200, {
    initialValue: 0,
    maxValue: 2
  });
  createController();
  httpBackend.flush();
  expect(scope.value).toEqual(0);
  expect(scope.maxValue).toEqual(2);
});

Other bits of ngMock


$timeout and $interval are decorated with flush() methods

Testing Other Components

Services Easy, Make sure to clean up singletons!
Filters Easy, Tailor made for unit testing
Directives More difficult

Testing Filters


describe('Capitalize Filter', function() {
  var filter;

  beforeEach(inject(function($filter) {
    filter = $filter('capitalize');
  }));

  it('capitalizes various strings', function() {
    var result;
    
    result = filter('unit tests');
    expect(result).toEqual('Unit Tests');

    result = filter('UNIT TESTS');
    expect(result).toEqual('Unit Tests');
  });
});

Tips for Directives

  • Use templateUrl for all non-trivial templates
  • Use html2js preprocessor and $templateCache
  • Consider separating logic from DOM manipulation
    using controller property
  • Wrap transclude directives in a div in your tests

it('tests myDirective', function() {
  var element = $compile('<my-directive></my-directive>')($rootScope);

  // Fire watches so template expressions are executed
  $rootScope.$digest();

  // Test element to verify DOM structure/contents

  // Test functions and values on $rootScope like with a controller
});

Making your code testable

(Angular is NOT idiot proof)

  • Do not manipulate DOM in controllers
  • Do not hide functions that need to be tested
  • Keep functions small and purposeful
  • Avoid monolithic directives
  • Write your tests before you get too far along

Coverage Goals


Lots of variables.
Decide for yourself.

Where do e2e tests fit in?


End-to-end tests cover user stories
Unit tests cover them at a different level of granularity
Neither is likely to be sufficient on its own

Thank You!

Questions?








Aaron N. Smith, @aaron_n_smith

Unit Testing with AngularJS

By Aaron N. Smith

Unit Testing with AngularJS

Meetup presentation on unit testing AngularJS apps.

  • 1,348