Angular Testing Part 3: Testing Recipes

Dave Ceddia bio photo By Dave Ceddia Comment

If you’re just joining, you may want to check out Part 1: Karma Setup and Part 2: Jasmine Syntax.

Overview

In the previous 2 articles, we set the stage:

You aren’t writing tests for your Angular app. The code base is growing and you’re feeling more apprehensive each passing day. You’d like to start testing, but where do you start?

It’d be awesome if you could start writing tests a few at a time. They’d build up a scaffold of safety around parts of your code, and you could begin to refactor those parts with total confidence.

In Part 1, we set up Karma and Jasmine: your Angular testing environment. And we wrote the first test!

In Part 2, we looked at Jasmine’s API – the anatomy of a test, including describe, beforeEach, it, and expect.

In this article we’ll look at recipes that you can apply for testing the various components in your app: the services, controllers, and directives.

We’ll also look at how to test code that uses promises, and how to mock services so that you can test isolated pieces.

Let’s dive in. Here’s what we’ll cover (jump around if you like):

TL;DR?

I know this is a lot to read at once. If you'd rather have it as a PDF, sign up and I'll send you the full 27-page Guide to Testing Angular (Parts 1, 2, and 3) including a Jasmine 2 Cheat Sheet.

Test Recipe: Service

Testing a service method is the simplest kind of test, so we’ll start here. In fact, you’ve already seen (and written) a test like this if you worked through Part 1.

Note: When I say “service” I really mean “service or factory” (if you’re not sure about the difference, read this article)

A service exposes some public methods:

angular.factory('userApi', function($http) {
  return {
    getUser: getUser,
    getFullName: getFullName
  };

  function getUser(id) {
    return $http.get('/users/' + id);
  }

  function getFullName(user) {
    return user.firstName + " " + user.lastName;
  }
});

Each method will get at least one test – more if it’s complicated by conditional logic.

describe('userApi', function() {
  // Step 1: Import the module this service belongs to
  beforeEach(module('myapp.users'));
  // Step 2: Inject the service you're testing (and other utils)
  var userApi, $httpBackend;
  beforeEach(inject(function(_userApi_, _$httpBackend_) {
    userApi = _userApi_;
    $httpBackend = _$httpBackend_;
  }));

  // Step 3: Test the methods
  it('should get users', function() {
    // a) "Given": Set up preconditions
    $httpBackend.expect('GET', '/users/42').respond(200);
    
    // b) "When": call the method under test
    userApi.getUser(42);

    // c) "Then": verify expectations
    expect($httpBackend.flush).not.toThrow();
  });

  it('should return full name', function() {
    // a) "Given" this user...
    var user = {firstName: "Dave", lastName: "Ceddia"};

    // b) "When" we call getFullName, 
    // c) "Then" it should return the user's name
    expect(userApi.getFullName(user)).toEqual("Dave Ceddia");
  });
});
This is the first time we've used $httpBackend. It allows us to mock HTTP calls and set up expectations for them. We won't go into it in depth here, but you can learn more about $httpBackend in this great article by Brad Braithwaite.

This pattern, or some variation on it, will be present in all your tests.

  1. Import the module that contains the service you’re testing.
  2. Inject the service you’re testing, and save it for later use. You may also want to set up mocks or spies at this point.
  3. Write the tests. Each one should ideally follow the pattern of Given/When/Then, an idea from BDD (Behavior-Driven Development):
  • Given some particular state of my app
    • set up state, mock or spy functions if necessary
  • When I call some method
    • call the method you’re testing
  • Then that method behaves in a certain way
    • verify the method did the right thing

In an ideal world, you’ll have one assertion per test (one expect(...) within each it). This doesn’t always work out, but try to stick to it if you can. Your tests will probably be easier to read.

If you find yourself violating the one-assertion-per-test rule frequently, it might be a sign that your methods are doing too much. Try simplifying those methods by breaking out behavior into other ones. Each method should be responsible for a single thing.

Test Recipe: Controller

When testing a controller, the recipe is very similar to testing a service, except that you need the controller function itself. Angular doesn’t allow you to inject controllers, though. That’d be too easy. So how do you get it?

Using the $controller service! Inject that, then use it to instantiate your controller.

Say your controller looks like this:

angular.controller('EasyCtrl', function() {
  var vm = this;

  vm.someValue = 42;
  vm.getMessage = function() {
    return "easy!";
  }
});

Then in your test:

describe("EasyCtrl", function() {
  // 1. Import the module
  beforeEach(module('myapp.users'));

  // 2. Inject $controller
  var EasyCtrl;
  beforeEach(inject(function($controller) {
    // 3. Use $controller to instantiate the controller
    EasyCtrl = $controller('EasyCtrl');
  }));

  // 4. Test the controller
  it("should have a value", function() {
    expect(EasyCtrl.someValue).toEqual(42);
  });

  it("should have a message", function() {
    expect(EasyCtrl.getMessage()).toEqual("easy!");
  });
});

That was pretty simple, right? Really similar to testing a service, except you need the extra step of injecting $controller and then calling it with the name of your controller.

Controller Recipe 2: $scope

But what if your controller depends on $scope? Well, you might want to think of converting it to use controllerAs… but maybe that’s not in the cards right now. Deadlines and stuff.

angular.controller('ScopeCtrl', function($scope) {
  $scope.someValue = 42;
  $scope.getMessage = function() {
    return "scope!";
  }
});

Here’s the test:

describe("ScopeCtrl", function() {
  // 1. Import the module
  beforeEach(module('myapp.users'));

  // 2. Inject $controller and $rootScope
  var ScopeCtrl, scope;
  beforeEach(inject(function($controller, $rootScope) {
    // 3. Create a scope
    scope = $rootScope.$new();

    // 4. Instantiate with $controller, passing in scope
    ScopeCtrl = $controller('ScopeCtrl', {$scope: scope});
  }));

  // 5. Test the controller
  it("should have a value", function() {
    expect(scope.someValue).toEqual(42);
  });

  it("should have a message", function() {
    expect(scope.getMessage()).toEqual("scope!");
  });
});

What’s different here?

$rootScope

We need to be able to create a scope object to pass in. $rootScope can do that for us with its $new method.

2nd argument to $controller

The 2nd argument specifies what to inject into the controller. It’s an object where the keys match the arguments to your controller function, and the values are what will be injected.

It’s worth noting that you don’t need to provide every injected parameter in that object. Angular’s dependency injector is still working for you, and it’ll inject what it can. It can’t inject $scope though, so if you forget to provide it, you’ll get some error like:

Error: [$injector:unpr] Unknown provider: 
   $scopeProvider <- $scope <- YourControllerName

This also applies to arguments provided by UI-Router, if you’re using it.

Tests use scope

The tests now use the scope object instead of the controller itself. (I kept the test similar to the old one so you could see the differences easily, but you could actually remove the ScopeCtrl variable entirely)

Controller Recipe 3: bindToController and initialization

If this is a directive’s controller, you might be passing values to it via bindToController and directive attributes.

You also might be running some initialization code when the controller first fires up. If you try to test that code using the previous recipes, you’ll notice that your tests run too late: the initialization has already run. If your init code depended on attributes passed via the directive, you’re hosed.

How can you get in front of that initialization code?

$controller actually takes a third argument: the bindings. You can pass those in before the controller runs.

angular.controller('BindingsCtrl', function() {
  var vm = this;

  activate();

  // Compute something based on a bound variable
  function activate() {
    vm.doubledNumber = vm.number * 2;
  }
});

Here’s the test:

describe("BindingsCtrl", function() {
  // 1. Import the module
  beforeEach(module('myapp.users'));

  // 2. Inject $controller
  var BindingsCtrl, scope;
  beforeEach(inject(function($controller) {
    // 3. Instantiate with $controller, passing in bindings
    BindingsCtrl = $controller('BindingsCtrl', {}, {number: 21});
  }));

  // 4. Test the controller
  it("should double the number", function() {
    expect(BindingsCtrl.doubledNumber).toEqual(42);
  });
});

For the 3rd argument to $controller, we passed an object where the keys are the binding names. When the controller started up, this.number was already set.

Test Recipe: Promises

Promises throw a wrench into the works: their asynchronous nature means they’re more difficult to test. As you’ll see though, they’re not too bad, as long as you remember to run that digest cycle.

This code returns a pre-resolved promise with $q.when:

angular.factory('promiser', function($q) {
  return {
    getPromise: function(value) {
      return $q.when(value);
    }
  };
});

Now for the test:

describe("promiser", function() {
  // 1. Import the module
  beforeEach(module('myapp.users'));

  // 2. Inject the service, plus $rootScope
  var promiser, $rootScope;
  beforeEach(inject(function(_promiser_, _$rootScope_) {
    // 3. Save off the things we need
    promiser = _promiser_;
    $rootScope = _$rootScope_;
  }));

  // 4. Test it
  it("should promise me a value", function() {
    // 5. Set up a value to receive the promise
    var returnValue;

    // 6. Call the promise, and .then(save that value)
    promiser.getPromise(42).then(function(val) {
      returnValue = val;
    });

    // 7. Run the digest function!!!1
    $rootScope.$digest();

    // 8. Check the value
    expect(returnValue).toEqual(42);
  });
});

Did I mention you need to run the digest function? Ok good, I thought I did.

Notice how the digest needs to be run before the expect call. If you try to inspect returnValue any time before running that digest, it’ll still be undefined.

Before we move on, let me draw your attention to Step 7: Run the digest function!!!1. You will probably forget this one day, and you will pull your hair out wondering why your F#!$ng tests aren’t passing. It’s very sneaky. Try not to leave it out.

Testing code that takes a Promise

If you need to test a function that takes a promise as an argument, you can create one easily with the $q service.

  1. Inject $q into your test
  2. Call $q.when(someValue), which creates a resolved promise that will pass someValue to the .then function.
  3. Make sure to include a call to $rootScope.$digest() at the appropriate time, to trigger any .then handlers.

Test Recipe: Directive

Testing directives can seem like a pain, and honestly a lot of the pain is in forgetting to call the digest function.

They are a bit more work to test than other parts of Angular, because they require a bit more boilerplate-y setup. And if you need to test the presence or absence of child elements, you’re venturing into the land of jQuery (or jqLite) selectors – debugging those can be troublesome.

Here’s a simple directive that takes a user object and displays its first and last name:

angular.directive('fullName', function() {
  return {
    scope: {
      user: '='
    },
    template: '<span>{{user.firstName}} {{user.lastName}}</span>'
  };
});

And here’s the test:

describe("fullName", function() {
  // 1. Load the module
  beforeEach(module('myapp.users'));

  // 2. Inject $rootScope and $compile
  var scope, element;
  beforeEach(inject(function($rootScope, $compile) {
    // 3. Set up the scope with test data
    scope = $rootScope.$new();
    scope.user = {
      firstName: "Dave",
      lastName: "Ceddia"
    };

    // 4. Create an element
    element = angular.element('<full-name user="user"></full-name>');

    // 5. Compile that element with your scope
    element = $compile(element)(scope);

    // 6. Run the digest cycle to ACTUALLY compile the element
    $rootScope.$digest();
  }));

  // 7. Test that it worked
  it("should display the full name", function() {
    // 'element' is a jqLite or jQuery element
    expect(element.text()).toEqual("Dave Ceddia");
  });
});

Play around with it a little and see how it breaks.

If you forget the $compile, it fails – the element is empty.

If you forget the $digest, it fails – the element’s contents are {{user.firstName}} {{user.lastName}}.

The element returned by angular.element is in fact a jqLite element (or a real jQuery one, if you’ve included jQuery in your karma.conf.js file). So you can verify things like presence of child elements, or that ng-class assigns the right classes, or that nested directives are evaluated or not evaluated.

Nested directives

Speaking of nested directives: they will only evaluate if their module has been loaded.

After the $compile and $digest run, the nested directives will remain untouched if their respective modules haven’t been loaded by a beforeEach(module(...)) call.

So if you’re testing some sort of <profile><name></name><age></age></profile> contraption, decide whether you want to test the inner elements and include their modules if so.

That wraps up the test recipes! Let’s talk a little about when to test…

Philosophy/Religion: Test First or Test Later?

Opinions on TDD (Test-Driven Development) range from “Are we still talking about that? I thought everyone figured out what a waste of time it is” to “TDD saves time and reduces stress. What’s not to like?”

Ultimately, you need to make your own decision. If you’ve never tried TDD, it’s worth giving it a shot. Be aware that it does require a bit of practice.

Just because you know how to write some tests doesn’t mean TDD will feel natural immediately. Make a committed effort: try it for a week, resolve to push through the feelings of awkwardness in the beginning, and then make an informed decision.

Personally, I find TDD to be fun sometimes. But I don’t always write tests first. It depends on my mood.

It’s not “all or nothing” here, either. You can break out TDD for difficult-to-design code, or maybe you’ll go through phases where you use it a lot and then don’t do it for weeks.

Where to go from here?

You’ve got enough knowledge to start testing your app now. There’ll be other stuff you’ll want to look into – spies and mocks are among the first – but this is a solid base to work from.

Start small, and write tests to cover your code little by little.

I wouldn’t recommend going on a testing rampage and writing nothing-but-tests for 2 weeks straight. This is more of a long-term thing. Don’t feel like you have to get it all done at once.

Start off writing 1 or 2 tests per day, maybe.

Once that feels comfortable, work up to a few more. Build up your habit of testing, and soon enough your app will have a scaffold of safety surrounding it. You’ll be able to refactor at will, and make changes fearlessly.


Do you want to hear more about spies and mocks? Would screencasts make these concepts easier to digest? Let me know in the comments, or hit me up on Twitter. Thanks for reading!

comments powered by Disqus