Unit testing the AngularJS code in the RAW Stack

In the previous post we refactored the JavaScript code for our AngularJS controller a bit to make it more testable. However we didn’t actually start writing any tests yet so lets create a few tests.


The AngularJS controller under test

Just as a quick reminder the AngularJS controller in our previous code was as follows

   1: (function () {

   2:     'use strict';

   3:     var module = angular.module("myApp", []);


   5:     module.controller("moviesCtrl", function ($scope, $http) {

   6:         $http.get("/api/movies").then(function (e) {

   7:             $scope.movies = e.data;

   8:         });


  10:         $scope.newMovie = { Title: "" };

  11:         $scope.addMovie = function () {

  12:             $http.post("/api/movies", $scope.newMovie).then(function () {

  13:                 window.location = window.location;

  14:             });

  15:         };

  16:     });

  17: }());


Nothing very complicated there.


What do we want to test

The first thing we need to think about what to test. There isn’t really a lot of application logic here so there is not much to test on that front. There is however something else and that is the public interface. In a strongly typed language like C# the compiler will make sure that the public interface of our code that is use by another piece of the application actually makes sense. If we rename a method or property but not the consuming code we will get a compile time error and quick feedback.

With JavaScript there is no compiler to validate this so it makes sense to create small unit tests that check our public interface. Should we accidently change it our tests will fail and warn us about this problem instead of us heaving to wait until runtime to detect it.


Testing the controllers public interface

It turns out that the public interface of our controller is exposed through the $scope, the object we use to make objects available to the scope. Looking at the controller we can see that there are three parts:

  1. The movies collection with existing movies.
  2. The newMovie object to create a new movie.
  3. The addMovie function to ask the server to insert a new movie.

One additional test I like to make is to see if the controller can be created. Just a sort of sanity check, if that fails looking any further is pointless and this needs to be fixed first.


Choosing a unit test framework and running tests

Even though you are free to choose the unit test framework there is a case to be made for using Jasmine. There are some really good other unit test framework like QUnit or Mocha as well as others. However the AngularJS team standardized on Jasmine and actually provide some useful additions to it like being able to run only a single test by naming it iit() or a single describe block by renaming it to ddescribe(). Both are really useful when focusing on the reason a test fails.


We also need to run the tests. Again there are many ways to do so but one way I really like is Karma, another product from the AngularJS team. Karma is a NodeJS application that will watch for files changing and when it notices a change it will automatically reload the files and run the tests. Using Karma all I need to do is keep it running on a second monitor, edit a JS file, save the changes and see Karma report on the status of my tests. A really nice and productive workflow.


Another nice option if Karma is that it is easy to integrate it with a continuous integration build system and run all JavaScript unit tests as part of the build and will fail it the build when needed. More about that later in another post.


Karma needs a configuration file to know how to run tests and what files to load. This is pretty easy to create by running Karma Init. Once the configuration file is in place the unit tests can be run with Karma Start.


The default Karma configuration file, karma.conf.js, we are using looks like this:

   1: module.exports = function(config) {

   2:   config.set({

   3:     basePath: '',

   4:     // frameworks to use

   5:     frameworks: ['jasmine'],

   6:     // list of files / patterns to load in the browser

   7:     files: [

   8:       './Scripts/angular.js',

   9:       './Scripts/angular-mocks.js',

  10:       './app/**/*.js',

  11:       '../RawStack.Tests/app/**/*.js'

  12:     ],

  13:     // test results reporter to use

  14:     // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'

  15:     reporters: ['progress'],

  16:     // enable / disable watching file and executing tests whenever any file changes

  17:     autoWatch: true,

  18:     // Start these browsers, currently available:

  19:     browsers: ['PhantomJS'],

  20:     // Continuous Integration mode

  21:     // if true, it capture browsers, run tests and exit

  22:     singleRun: false

  23:   });

  24: };

The real file is a bit longer, see GitHub, but this captures the most important parts.

Testing if the controller can be created

A simple Jasmine test to make sure the movies controller can be created is as follows.


   1: describe("The moviesCtrl", function () {

   2:     'use strict';


   4:     beforeEach(module("myApp"));


   6:     it("can be created", inject(function ($controller) {

   7:         var scope = {};

   8:         var ctrl = $controller("moviesCtrl", {

   9:             $scope: scope

  10:         });


  12:         expect(ctrl).toBeDefined();

  13:     }));

  14: });


Testing the public API on the $controller

Testing most of the API on the $scope object is not a lot harder and can be done as follows.

   1: describe("The moviesCtrl scope", function () {

   2:     'use strict';


   4:     var scope;


   6:     beforeEach(module("myApp"));


   8:     beforeEach(inject(function ($controller) {

   9:         scope = {};

  10:         $controller("moviesCtrl", {

  11:             $scope: scope

  12:         });

  13:     }));


  15:     it("has a newMovie object", function () {

  16:         expect(scope.newMovie).toEqual({ Title: "" });

  17:     });


  19:     it("has a addMovie function", function () {

  20:         expect(typeof scope.addMovie).toBe("function");

  21:     });

  22: });


What is missing

You might have notices that I mentioned three parts to the public API on the $scope but I am only testing two of those three. The thing I am not testing is the existence of the movies collection.

The reason is that this is not created until our $http request is done. Now maybe I should have created the empty collection directly and inserted new movies when the HTTP request was done but that is not the most important part here. You might be surprised to see the test pass even though they do an HTTP request to a server that, in a unit test, doesn’t actually exist. Yet there is no error.

This is due to the fact that we loaded angular-mocks.js in our Karma configuration file. This creates a fake implementation of the $httpBackend service. This is the service that normally does the actual HTTP request on the wire. In this case the mock service intercepts the call and never actually responds to it so the movies collection is not created but there is no error either.

In the next post I will show how to use the $httpBackend service and actually make it respond to the request.


The real problem however is in using the $http service directly from an AngularJS controller. While technically very possible as we have seen it is far better not to do so and move our HTTP code into a separate service and use that instead. This keeps the logic in the controller down and makes it much easier to test.


Try it

The running application can be found here and the source on GitHub here.



Testing any business application is important but with a dynamic language like JavaScript it is even more important that in other cases. Where the C# compiler catches some errors for us these would not be detected until runtime with JavaScript. So make sure to unit test all your code well.


Index of posts on the RAW stack

See here for more posts on the RAW stack.



Leave a Reply

Your email address will not be published. Required fields are marked *