Testing Angular.js

03/08/2013

As an Angular.js developer, I want to test my code, so that I can feel reasonably good about it. This sounds like a "User Story" in Scrum but it's more of a developer quest. So in this blog we will look at ways to test our Angular.js code.

Let's see what we need -

  • We need to write unit tests for our
    • Controllers
    • Services
    • Directives
    • Filters
  • We need to write end-to-end tests so that we can be sure everything is working well together

To kick off things, lets build a simple app which has a -

  • Login page, on which the user enters a username / password
  • Main page, which is shown if the user is successfully logged in
  • On the main page, we fetch data from a server and show it to the user

Simple enough, this would look something like -

Login Main

We have 2 controllers and 2 services in this setup -

controllers/login.coffee

'use strict'

class LoginCtrl

  constructor: (@$scope, @$location, @webService, @helperService) ->
    @setup()

  setup: ->
    @$scope.login = @login

  success: (response) =>
    @helperService.setName "Rocky" #maybe this will come from reponse
    @$location.url "main"

  error: (response) =>
    @$scope.message = "Login Failed"

  login: (user) =>
    promise = @webService.login(user)
    promise.then @success, @error

LoginCtrl.$inject = ["$scope", "$location", "webService", "helperService"]
angular.module("demoApp").controller "LoginCtrl", LoginCtrl

controllers/main.coffee

'use strict'

class MainCtrl

  constructor: (@$scope, @webService, @helperService) ->
    @setup()

  setup: ->
    @$scope.name = @helperService.getName()

    promise = @webService.getData()
    promise.then @success, @error

  success: (response) =>
    #do something with data

  error: (response) =>
    @$scope.message = "Error!"

MainCtrl.$inject = ["$scope", "webService", "helperService"]
angular.module("demoApp").controller "MainCtrl", MainCtrl

services/webservice.coffee

'use strict'

class WebService

  constructor: (@$http) ->

  login: (user) ->
    @$http.post("http://localhost:3000/login", user)

  getData: () ->
    @$http.get("http://localhost:3000/getData")

angular.module "demoApp.webService", [], ($provide) ->
  $provide.factory "webService", ["$http", ($http) -> new WebService($http)]

services/helperservice.coffee

'use strict'

class HelperService

  constructor: () ->

  setName: (name) ->
    @name = name

  getName: ->
    @name

angular.module "demoApp.helperService", [], ($provide) ->
  $provide.factory "helperService", -> new HelperService()

Now, a piece of advice. We must try to keep our controllers lean and move logic in a OO or functional manner to services. As we will see services are easier to test.

Second point worth noting is that to run tests etc. we need a build system. I recommend Yeoman which uses the excellent Grunt task runner. So to setup a Yeoman + Angular project, you can check out the Yeoman site.

From hereon, I assume that we are using the Yeoman setup. The default Yeoman setup gives us a sample controller unit test in Jasmine, which we can modify to this -

spec/controllers/main.coffee

'use strict'

describe 'Controller: MainCtrl', () ->

  # load the app module
  beforeEach module 'demoApp'

  MainCtrl = {}
  scope = {}

  # Initialize the controller and a mock scope
  beforeEach inject ($injector, $controller, $rootScope) ->
    scope = $rootScope.$new()

    #Set up the mock http service responses
    $httpBackend = $injector.get('$httpBackend')
    $httpBackend.when('GET', 'http://localhost:3000/getData').respond({username: 'userX'}, {'A-Token': 'xxx'})

    MainCtrl = $controller 'MainCtrl', {
      $scope: scope
    }

  it 'should set scope properly', () ->
    expect(MainCtrl.scope).not.toBe null

  it 'should get services properly', () ->
    expect(MainCtrl.webService).not.toBe null

Two main things to note are -

  • the use of Angular's dependency injection services which gives us a reference to the $injector itself which we use to mock the HTTP service (which is used by the MainCtrl's constructor)
  • and a reference to the controller itself

Since we kept our Controllers lean, there is nothing much to test. To test the services is much easier -

spec/services/helper.coffee

'use strict'

describe 'Service: helperService', () ->

  # load the app module
  beforeEach module 'demoApp'

  helperService = {}

  # Initialize the controller and a mock scope
  beforeEach inject ($injector) ->
    helperService = $injector.get('helperService')

  it 'should get data properly', () ->
    helperService.setName("rocky")
    expect(helperService.getName()).toBe "rocky"

Once again the $injector does the hard work of giving us our service's reference and from there on it's an easy task.

So we are done with writing some unit tests, you can run "grunt test" to test them.

Time to write some end-to-end tests. Unfortunately at the time of writing this blog, Yeoman does not fully support running end-to-end tests out of the box. It gives us a Karma end-to-end config file but does not provide a mechanism to run it.

To run it, we need to do a few changes to our Gruntfile.js -

Remove the grunt:test task and add these new two tasks

grunt.registerTask('test:unit', [
  'clean:server',
  'concurrent:test',
  'connect:test',
  'karma:unit'
]);

grunt.registerTask('test:e2e', [
  'clean:server',
  'concurrent:server',
  'connect:livereload',
  'karma:e2e'
]);

Then modify the karma task like this

...
karma: {
  unit: {
    configFile: 'karma.conf.js',
    singleRun: true
  },
  e2e: {
    configFile: 'karma-e2e.conf.js',
    singleRun: true
  }
},
...

In the karma-e2e.conf.js file, uncomment the last two lines -

// Uncomment the following lines if you are using grunt's server to run the tests
proxies = {
   '/': 'http://localhost:9000/'
};
// URL root prevent conflicts with the site root
urlRoot = '_karma_';

We now have two Grunt tasks to run Unit and E2E tests respectively - grunt test:unit and grunt test:e2e

Let us write a E2E test -

test/e2e/smoke.coffee

'use strict'

describe 'SmokeTest', () ->

  it 'should check if login page is working', () ->
    browser().navigateTo("/")
    expect(element('h2').html()).toEqual("Login")

  it 'should allow login', () ->
    browser().navigateTo("/")
    input("user.username").enter("rocky")
    input("user.password").enter("1234")
    element(".btn").click()
    expect(browser().window().hash()).toEqual("/main")

Like I mentioned we can run the E2E tests by running grunt test:e2e, this task first starts the test http server and then the tests are run over that.

That's it, we have run our Unit and End-to-End tests successfully. Happy hacking with Angular.js.