AngularJS: Tests

2013-05-16 00:00:00 +0100 by Alex R. Young


In the last part we changed the app to support multiple feeds.

This week you'll learn how to write a short unit test to test the app's main controller. This will involve mocking data.

If you get stuck at any part of this tutorial, check out the full source here: commit 7b4bda.

Neat and Tidy Tests

The goal of this tutorial is to demonstrate one method for writing neat and tidy tests. Ideally mocked data should be stored in separate files and loaded when required. What we absolutely don't want is global variables littering memory.

To run tests with the Yeoman-generated app we've been working on, type grunt test. It'll use Karma and Jasmine to run tests through Chrome using WebSockets. The workflow in the console is effortless, despite Chrome appearing and disappearing in the background (it won't trample on your existing Chrome session, it'll make a separate process). It doesn't steal focus away, which means you can invoke tests and continue working on code without getting interrupted.

My workflow: mock, controller, test, and a terminal for running tests

The basic approach is to use $httpBackend.whenJSONP to tell AngularJS to return some mock data when the tests are run, instead of fetching the real feed data from Yahoo. That sounds simple enough, but there's a slight compilation: leaving mock data in the test sucks. So, what do we do about this? The karma.conf.js file that was created for us by the Yeoman generator contains a line for loading files from a mocks directory: 'test/mock/**/*.js. These will be loaded before the tests, so let's dump some JSON in there.

Interestingly, if you run grunt test right now it'll fail, because the app makes a JSONP request, and the angular-mocks library will flag this as an error. Using $httpBackend.whenJSONP will fix this.

JSON Mocks

Open a file called test/mock/feed.js (you'll need to mkdir test/mock first), then add this:

'use strict';

angular.module('mockedFeed', [])
  .value('defaultJSON', {
    query: {
      count: 2,
      created: '2013-05-16T15:01:31Z',
      lang: 'en-US',
      results: {
        entry: [
            title: 'Node Roundup: 0.11.2, 0.10.6, subscribe, Omelette',
            link: { href: 'http://dailyjs.com/2013/05/15/node-roundup' },
            updated: '2013-05-15T00:00:00+01:00',
            id: 'http://dailyjs.com/2013/05/15/node-roundup',
            content: { type: 'html', content: 'example' }
            title: 'jQuery Roundup: 1.10, jquery-markup, zelect',
            link: { href: 'http://dailyjs.com/2013/05/14/jquery-roundup' },
            updated: '2013-05-14T00:00:00+01:00',
            id: 'http://dailyjs.com/2013/05/14/jquery-roundup',
            content: { type: 'html', content: 'example 2' }

This uses angular.module().value to set a value that contains some JSON. I derived this JSON from Yahoo's API by running the app and looking at the network traffic in WebKit Inspector, then edited out the content properties because they were huge (DailyJS has full articles in feeds).

Loading the Mocked Value

Open test/spec/controllers/main.js and change the first beforeEach to load mockedFeed:

beforeEach(module('djsreaderApp', 'mockedFeed'));

The beforeEach method is provided by Jasmine, and will make the specified function run before each test. Now the defaultJSON value can be injected, along with the HTTP backend:

var MainCtrl, scope, mockedFeed, httpBackend;

// Initialize the controller and a mock scope
beforeEach(inject(function($controller, $rootScope, $httpBackend, defaultJSON) {
  // Set up the expected feed data
  httpBackend = $httpBackend;

  scope = $rootScope.$new();
  MainCtrl = $controller('MainCtrl', {
    $scope: scope

You should be able to guess what's happening with $httpBackend.whenJSONP(/query.yahooapis.com/) -- whenever the app tries to contact Yahoo's service, it'll trigger our mocked HTTP backend and return the defaultJSON value instead. Cool!

The Test

The actual test is quite a comedown after all that mock wrangling:

it('should have a list of feeds', function() {
  expect(scope.feeds[0].items[0].title).toBe('Node Roundup: 0.11.2, 0.10.6, subscribe, Omelette');

The test checks $scope has the expected data. httpBackend.flush will make sure the (fake) HTTP request has finished first. The scope.feeds value is the one that MainCtrl from last week derives from the raw JSON returned by Yahoo.


You should now be able to run grunt test and see some passing tests (just like in my screenshot). If not, check out djsreader on GitHub to see what's different.

Most of the work for this part can be found in commit 7b4bda.