Building VTS

The Official VTS Engineering Blog

Testing Your AngularJS App

By Alexander Frankel

Front-End Testing

Ever have the feeling that you have written some code that may or may not work some of the time? …wait, what?

Gustavo: “Oh Alex, what did you work on today?” Me: “I created this new angular service that ensures only one dropdown is open at a time. It’s awesome!” Me: “Come check it out.”

Imgur

Gustavo: “Wait, so it makes sure that there can be two open at the same time?” Me: “crap”

That’s why we write tests…even on the front-end. Maybe you currently don’t test front-end functionality in your app, maybe you dont test your server-side code, maybe you hate testing in general.Well… try Karma dude (it’s good for the soul)!

Karma is a javascript test-runner and works like magic with your Angular app. You are relieved from setting up lots of configuration and are provided with instantaneous feedback. And we all know feedback === productivity === creativity === happiness :)

Let’s break down an example of testing in Angular, showing how Karma can put a smile on your face.

First things first: Install Karma Using npm

1
$ npm  install --save-dev karma

Note: the --save-dev flag above adds the Karma package to your devDependencies in your package.json file. This indicates to other developers that Karma is needed in order to develop this project.

Karma works by launching a browser, loading our app into the browser, and running our tests against our app…therefore, we need to tell Karma about all of our files.

Generate the configuration file and fill in each section using:

1
$ karma init karma.conf.js

The config file that is generated will probably look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
module.exports = function(config) {
  config.set({

    // base path, that will be used to resolve files and exclude
    basePath: '',

    // frameworks to use
    frameworks: ['mocha'],
    },

    // list of files / patterns to load in the browser
    files: [
      'app/components/angular/angular.js',
      'app/components/angular-mocks/angular-mocks.js',
      'app/scripts/**/*.js',
      'test/spec/**/*.js'
    ],

    // list of files to exclude
    exclude: [],

    // test results reporter to use
    // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
    reporters: ['spec'],

    // web server port
    port: 8080,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,

    // Start these browsers, currently available:
    browsers: [
      'Chrome'
    ],

    // If browser does not capture in given timeout [ms], kill it
    captureTimeout: 60000,

    // report which specs are slower than 100ms
    // CLI --report-slower-than 100
    reportSlowerThan : 100,

    // Continuous Integration mode
    // if true, it capture browsers, run tests and exit
    singleRun: false
  });
};

Note: angular-mocks.js and the rest of our angular code needs to be available to reference inside the karma.conf.js file. Note: since we have set autoWatch: true, anytime we edit/change a file, Karma will automatically re-run our test-suite. Talk about instantaneous feedback…

Now we can run Karma by entering the follow line in the command-line:

1
$ karma start karma.conf.js

The Code We Want to Test

According to our config file above, we will be using Mocha as our testing framework. Just a quick side-note…when it comes to front-end development, I typically go in with a strategy, write my code, and test after. Many times, I find that I make front-end implementation decisions on the fly, allowing for channeled spontaneity and creativity. Some may argue for TDD here, but because front-end developement requires an interation between functionality and design, I find that I am more productive writing my tests after implementation. So jump, take a dive… …and we’re in.

As you probably well know, Angular controllers are used to add functionality & data & information & behavior to the Angular Scope, which of course is used in the view. That being said, let’s think for a second how we might test an individual controller that is associated with a particular scope. Since Angular encourages, if not forces us to write modular code, we can certainly use this to our benefit in our tests. THIS IS THE BEST PART! We can actually test our controllers (and other angular components for that matter) in isolation!! So maybe we can somehow create comething called ‘scope’ in our test and give it some properties that mimic what the actual scope in our app might look like. Then, all we need to do is inject our controller into the test and hand it the mimicked scope that we just created. The controller is free to add/subtract whatever it needs or wants to from this scope object and we can test to make sure it is doing that correctly. Make sense?

If not, that’s okay. Let’s take a look…

For our example, lets consider a student database app with the ability to view all students in 1st, 2nd, or 3rd grade.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// This is the controller definition
// my-controller.js

var myApp = angular.module('myApp', []);

myApp.controller('MyController', ['$scope', '$http', function($scope, $http) {
  $scope.grades = ['first', 'second', 'third']
  $scope.selectedGrade = null;
  $scope.selectedStudents = [];
  $scope.allStudents = [];

  // select which grade you would like to see and load only students in that grade
  $scope.selectGrade = function(grade) {
    $scope.selectedGrade = grade;
    _selectStudents();
  };

  // reset selected grade and selected students
  $scope.deselectGrade = function() {
    $scope.selectedGrade = null;
    $scope.selectedStudents = [];
  };

  // select only the students in the grade that has been selected
  _selectStudents = function() {
    for(i=0; i<$scope.allStudents.length; i++) {
      if($scope.allStudents[i].grade === $scope.selectedGrade) {
        $scope.selectedStudents.push($scope.allStudents[i]);
      }
    }
  };

  // initially load in the students when the controller is initialized
  $http.get('/students').then(function(response) {
    $scope.allStudents = response.data.students;
  });
});
  • We start off with some grades to choose from.
  • Since we have not yet selected a grade, our selectedStudents object is an empty array.
  • We also have our allStudents array which will hold our index of students that returns from our call to the server at the bottom of the controller.
  • Our functions defined on scope allow us to select/deselect a grade and set our selected students accordingly.

Testing Our Controller

Back to our tests, we will want to create a scope object to mimic the actual scope, allowing us to test the interaction of our controller with it. Let’s check it out. Be sure to notice the easy to understand syntax that Mocha provides!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// contoller test
// spec/controllers/my-controller.js

describe('MyController', function() {

  // define some variables
  var scope, httpBackend, myController;

  // load in our app
  beforeEach(module('MyApp'));

  // code to execute before each of our tests
  beforeEach(inject(function($rootScope, $httpBackend, $controller) {
    httpBackend = $httpBackend;
    scope = $rootScope.$new();
    myController = $controller('MyController', {$scope: scope});

    // simulates a GET request to our server
    httpBackend.when('GET', '/students')
      .respond({students: [{name: "alex", grade: "third"},
                           {name: "tom", grade: "second"},
                           {name: "silvio", grade: "first"}
                          ]});
    httpBackend.flush();
  }));

  // code to execute after each of our tests
  afterEach(function() {
    httpBackend.verifyNoOutstandingExpectation();
    httpBackend.verifyNoOutstandingRequest();
  });

  // we can break each part of our controller down into smaller modules
  describe('initialization', function() {
    it('sets grades to default array of grades', function() {
      expect($scope.grades).to.equal(["first", "second", "third"]);
    });

    it('sets selectedGrade to null', function() {
      expect($scope.selectedGrade).to.be.null;
    });

    it('sets selectedStudents to empty array', function() {
      expect($scope.selectedStudents).to.equal([]);
    });

    it('sets allStudents to the index of students returned from the server', function() {
      expect($scope.allStudents).to.equal([{name: "alex", grade: "third"},
                                           {name: "tom", grade: "second"},
                                           {name: "silvio", grade: "first"}
                                          ]);
    });
  });

  describe('selecting a grade', function() {
    beforeEach(function() {
      $scope.selectGrade("third");
    });

    it('sets selectedGrade to the grade that was selected', function() {
      expect($scope.selectedGrade).to.equal("third");
    });

    it('sets selectedStudents to an array all students in the grade that was selected', function() {
      expect($scope.selectedStudents).to.equal([{name:"alex", grade:"third"}]);
    });
  });

  describe('deselecting a grade', function() {
    beforeEach(function() {
      $scope.deselectGrade();
    });

    it('resets selectedGrade to null', function() {
      expect($scope.selectedGrade).to.be(null);
    });

    it('resets selectedStudents to empty array', function() {
      expect($scope.selectedStudents).to.equal([]);
    });
  });

});

Let’s take this line-by-line and talk through exactly what is happening in our test. Before we start, notice the readability of the syntax here. Without further explanation, you already have a good idea of what we are testing.

After defining our test by giving it a name that describes what we are testing, we define a few variables that will be used later in our test. Of course, we need to load in the app that we are testing, so the beforeEach block helps us to to do this before running each test.

Now the fun stuff. This is key in understanding how testing in Angular is possible, so let’s take it slow and steady.

Earlier, I talked about creating a scope that mimics what our actual scope might look like. Thats where $rootScope comes in. If you are thinking, “whoa, we are actually creating a real, live scope here to use in our tests”, then you are correct. We are creating a brand new scope (with no parents or children since it is root). We are able to take that scope and give it to ‘MyController’ using the $controller service. An instance of ‘MyController’ is created and is now associated with this brand new scope that we just defined. Pretty cool, right?

Second part…we have this thing called $httpBackend. Angular gives this to us. It is a fake http backend implementation. Well, what the hell are we going to do with it? Good question. Again, Angular testing is awesome because we can break our code down into modules with no external dependencies. Therefore, putting the fate of our ‘MyController’ controller that knows nothing about some server millions of miles away seems to be a little out of our scope (no pun intended). What if the server is malfunctioning? What if it is configured wrong? The answer is that it doesn’t matter because $httpBackend allows us to create a mock of that server, completely seperating our controller’s concerns. In our example, we are using $httpBackend to simulate a successful server response. We define this response to be a JSON object with a ‘students’ property.

**Important: $httpBackend is also used in production. The $httpBackend service that we are using here is a mock. To elaborate further, the $httpBackend used in production responds to server requests asynchronously. In order to preserve the asynchronous nature of what is happening in production, we use $httpBackend.flush(). This allows us to make our simulated request and then receive the response when this method is called.

We also have defined an afterEach block. Here we are making sure that after each test all of our simulated server requests have been made and that none of our outstanding requests need to be flushed.

Initialization Test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
describe('initialization', function() {
  it('sets grades to default array of grades', function() {
    expect($scope.grades).to.equal(["first", "second", "third"]);
  });

  it('sets selectedGrade to null', function() {
    expect($scope.selectedGrade).to.be.null;
  });

  it('sets selectedStudents to empty array', function() {
    expect($scope.selectedStudents).to.equal([]);
  });

  it('sets allStudents to the index of students returned from the server', function() {
    expect($scope.allStudents).to.equal([{name: "alex", grade: "third"},
                                         {name: "tom", grade: "second"},
                                         {name: "silvio", grade: "first"}
                                        ]);
  });
});

Nothing too fancy here. We are testing that each property defined on scope is defined correctly upon controller initialization. Notice our test for students returned from the server. The array inside the allStudents property is the same array we defined as our response in the mock http request. Since we flushed the request, the response data is at our disposal.

Selecting a Grade Test
1
2
3
4
5
6
7
8
9
10
11
12
13
describe('selecting a grade', function() {
  beforeEach(function() {
    $scope.selectGrade("third");
  });

  it('sets selectedGrade to the grade that was selected', function() {
    expect($scope.selectedGrade).to.equal("third");
  });

  it('sets selectedStudents to an array all students in the grade that was selected', function() {
    expect($scope.selectedStudents).to.equal([{name:"alex", grade:"third"}]);
  });
});

Again, we are being very descriptive of what exactly we are testing. In our beforeEach block, we are calling the selectGrade function that we defined on scope in our controller and passing in “third” grade. From here, we expect two different things to happen. Number one, selectedGrade is set to “third” and the correct students are set to the selectedStudents property. This is the beauty of angular testing. Since we took our time in setting up the infrastructure for our tests, we are able to write straight-forward expectations.

Deselecting a Grade Test
1
2
3
4
5
6
7
8
9
10
11
12
13
describe('deselecting a grade', function() {
  beforeEach(function() {
    $scope.deselectGrade();
  });

  it('resets selectedGrade to null', function() {
    expect($scope.selectedGrade).to.be(null);
  });

  it('resets selectedStudents to empty array', function() {
    expect($scope.selectedStudents).to.equal([]);
  });
});

Here we are testing the de-selection functionality previously defined in our controller. Remember before how we set autoWatch: true in our Karma config file? Again, this means that if we change any file that has been loaded into Karma (production code or tests), our test suite will automatically re-run.

I’d just like to leave you with a protip that I use A LOT, especially in my larger applications. Sometimes running your entire test suite in Karma can take a while and be unnecessarily repetitive. For instance, if you have a couple hundred tests written, but are only working on one specific component, maybe you don’t need to run your entire test suite every time you make a change. I am pleased to introduce .only. This allows you to only run a specific file (if placed at the top of your test) or a specific test (if placed before a specific test), saving you lots of time. Have a look…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
describe.only('MyController', function() {

  // define some variables
  var scope, httpBackend, myController;

  // load in our app
  beforeEach(module('MyApp'));

  // code to execute before each of our tests
  beforeEach(inject(function($rootScope, $httpBackend, $controller) {
    httpBackend = $httpBackend;
    scope = $rootScope.$new();
    myController = $controller('MyController', {$scope: scope});

    // simulates a GET request to our server
    httpBackend.when('GET', '/students')
      .respond({students: [{name: "alex", grade: "third"},
                           {name: "tom", grade: "second"},
                           {name: "silvio", grade: "first"}
                          ]});
    httpBackend.flush();
  }));

  ...

)};

Only this file will run in the Karma test suite.

1
2
3
4
5
6
7
describe.only('selecting a grade', function() {
  beforeEach(function() {
    $scope.selectGrade("third");
  });

  ...
});

Only the tests nested under this describe block will run.

BE CAREFUL: about committing to your git repository without removing .only. If you run your committed code in a test runner before merging with production, all of your tests may not run. So remove the .only before committing!

Honestly, using Karma with Angular has delighted me to the fullest. Say what you want, but seeing those bright green checkmarks appear in front of me really keeps my stamina up, not to mention my entire day is brighter knowing that I am writing working, maintainable code. (Sometimes, I actually look more forward to writing tests more so than writing the actual implementation). Anyways cover your bases, test the front-end of your application, and have fun doing it!

…besides it’s good karma.

Btw, make sure to check out some of the awesome javascript testing libraries in the market including: Chai, and Sinon.js * Chai is awesome for assertions for the DOM and jQuery * Sinon.js is awesome for spying on functions