7: Adding unit testing using karma

Part 7, in which we create karma unit tests for the list and edit controllers, including mocking the http calls.

If you’ve jumped into the middle of this tutorial, you’ll need the code from github:PaulL:tutorial_6, or you might want to visit the index page or hit the tutorials link above and look in the rails 4 tutorial.

We two controllers to test and a shallow starting test.  Let’s sort out some more thorough tests in club.spec.js, using karma and jasmine. The content in this tutorial replaces the entire existing content of club.spec.js with a more sophisticated set of tests.

We’re going to first work on tests for the the base club controller. We’ll use the standard mock httpBackend service so that we aren’t connecting to our backend, as unit tests aren’t supposed to go through to the backend. We’re basing some of the process here on information from three pages:

We’re also going to use strong validation of our http traffic – so we’re validating each call to the server happens where we expect, and that no spare calls happen, following the expect syntax from the httpBackend documentation.

A final consideration is how we factor and group our code, and therefore which bits we have to retype, and which bits are automatically shared through the beforeEach clauses.  We’ve chosen to create one outer describe clause for the whole test file, with our custom mocks instantiated in the beforeEach for that describe block.  The controllers themselves are declared separately in a describe block for the base controller, and a different describe block for the edit controller. The following blocks together form the complete unit test set for this controller as it stands. First, the declarations and mocking of httpBackend:

/**
 * Unit tests for the club functionality
 */
describe( 'Club functionality', function() {
  // mock Application to allow us to inject our own dependencies
  beforeEach(angular.mock.module('league'));

  // create the custom mocks on the root scope
  beforeEach(angular.mock.inject(function($rootScope, _$httpBackend_, $state){
    //create an empty scope
    scope = $rootScope.$new();

    // we're just declaring the httpBackend here, we're not setting up expectations or when's - they change on each test
    scope.httpBackend = _$httpBackend_;
    scope.$state = $state;

These are really just definitions, we’ve created an empty root scope that we inject into the controller – it would normally expect to receive it’s root scope from app.js. We also pull in the httpBackend mock, which means we can fake traffic with our server rather than actually calling the server.

We’re choosing to put the _$httpBackend_ and $state onto the scope variable.  This is purely a convenience thing, it means we don’t have to keep declaring them as a dependency on each “it” block.  It should be possible to simply inject them to avoid this, but I find it a bit more intuitive to do it this way.

After each test we verify there are no outstanding http requests and that none of our expected http requests have failed to be called, to confirm that only the http traffic we expect is occurring.

  afterEach(function() {
    scope.httpBackend.verifyNoOutstandingExpectation();
    scope.httpBackend.verifyNoOutstandingRequest();
  });

In the next block we create the tests for the base club controller. We inject the controller itself, and we concern ourselves with the response from the ClubRes.query(), checking it’s correctly handled.  We create a precanned response whenever the clubs.json url is hit. We also drain this request from the queue – so we call scope.$digest to tell it to process asynch events, and we then call scope.httpBackend.flush() to drain the http request. This otherwise has to be done in each individual test, which duplicates code.

  describe( 'Clubs list controller', function() {  

    beforeEach(angular.mock.inject(function($controller){
      //declare the controller and inject our scope
      $controller('ClubsCtrl', {$scope: scope, $dialog: scope.fakeDialog});

      // setup a mock for the resource - instead of calling the server always return a pre-canned response
      scope.httpBackend.expect('GET', '../clubs.json').respond([
        {"contact_officer":"Contact Officer 1","created_at":"2012-02-02T00:00:00Z","date_created":"2012-01-01T00:00:00Z","id":1,"name":"Club 1","updated_at":"2012-03-03T00:00:00Z"},
        {"contact_officer":"Contact Officer 2","created_at":"2012-02-02T00:00:00Z","date_created":"2012-01-01T00:00:00Z","id":2,"name":"Club 2","updated_at":"2012-03-03T00:00:00Z"}
      ]);
      scope.$digest();
      scope.httpBackend.flush();
    }));

We create a block of tests that relate to the initial render. We’re just checking that we got two rows, and we check the value in one of the rows to make sure it’s kind of correct. We’re choosing not to verify every individual value, we’re figuring that would be a lot like testing the angular framework itself, which seems wasteful, we’re just doing enough that if it were seriously broken we’d notice.

    describe( 'Initial render', function() {
      it('Has two clubs defined', function(){
        expect(scope.clubs.length).toEqual(2);
      });

      it('First club\'s contact officer is as expected', function(){
        expect(scope.clubs[0].contact_officer).toEqual('Contact Officer 1');
      });
    });

You could close all the brackets at this point and run grunt build to see if it works. Or continue until you have the whole file (and deal with any typos or errors all in one go).

Next, we create the tests for the editClub method and the tests for the edit method.  These just call the $state service, we set up a Spy to verify that they call with the parameters that we want:

    describe('Other controller methods', function(){
      it('Calls edit on first row', function() {
        // we expect it to call $state
        spyOn(scope.$state, "transitionTo").andCallThrough();

        // call edit
        scope.editClub(scope.clubs[0]);

        // we expect the club state to be called, passing in the id of the first item
        expect(scope.$state.transitionTo).toHaveBeenCalledWith('club', { clubId: 1});
      });

      it('Calls new', function() {
        // we expect it to call $state
        spyOn(scope.$state, "transitionTo").andCallThrough();

        // call new
        scope.newClub();

        // we expect the club state to be called, passing in the id of the first item
        expect(scope.$state.transitionTo).toHaveBeenCalledWith('club');
      });
    });
  });

Next, we create tests for the edit controller. We reuse some mocks from the overall controller test.

  describe( 'Club detail controller', function() {
    //mock the controller
    beforeEach(function() {
      // mock the stateParams
      scope.fakeStateParams = {
        clubId: "2"
      };      
    });

We create a fake stateParams, so that we can set the clubId to whatever value we want. We need to set this before we instantiate the controller, so we can’t instantiate the controller in the beforeEach block.

    describe( 'Club detail controller base tests:', function() {  
      it('Initial detail controller render receives a club id, gets club, success', angular.mock.inject(function($controller){
        // use default clubId of 2
        $controller('ClubCtrl', { $scope: scope, $stateParams: scope.fakeStateParams });
        expect(scope.clubId).toEqual(2);

        scope.httpBackend.expectGET('../clubs/2.json').respond({"contact_officer":"Contact Officer 2","created_at":"2012-02-02T00:00:00Z","date_created":"2012-01-01T00:00:00Z","id":2,"name":"Club 2","updated_at":"2012-03-03T00:00:00Z"});

        scope.$digest();  
        scope.httpBackend.flush();
        expect(scope.club.name).toEqual('Club 2');     
      }));  

      it('Initial detail controller render does not receive a club id, creates new club, success', angular.mock.inject(function( $controller) {
        // set clubId to null
        scope.fakeStateParams.clubId = null;

        $controller('ClubCtrl', { $scope: scope, $stateParams: scope.fakeStateParams });
        expect(scope.clubId).toBeNaN();
      }));             
    });

We verify the initial render, first setting the clubId to a value, then setting it to null, and check the http traffic is as we expect.

    describe( 'Club detail controller update method tests', function() {
      beforeEach(angular.mock.inject(function($controller){
        $controller('ClubCtrl', {$scope: scope, $stateParams: scope.fakeStateParams});

        // The initial render triggers a get, drain that before we start the test proper
        scope.httpBackend.expectGET('../clubs/2.json').respond({"contact_officer":"Contact Officer 2","created_at":"2012-02-02T00:00:00Z","date_created":"2012-01-01T00:00:00Z","id":2,"name":"Club 2","updated_at":"2012-03-03T00:00:00Z"});

        scope.$digest();
        scope.httpBackend.flush();
       }));

      it('Submit with clubId calls put on server, put succeeds', function(){
        spyOn(scope.$state, "transitionTo").andCallThrough();

        scope.club.name = 'Changed name';     
        scope.submit();

        scope.httpBackend.expectPUT('../clubs/2.json').respond({});
        scope.$digest();
        scope.httpBackend.flush();
        expect(scope.$state.transitionTo).toHaveBeenCalledWith('clubs');
      });
    });

For the update tests, we create a before block that sets the state to a specified clubId, then create our tests proper. As we add error handling we’ll have more tests in here.

    describe( 'Club detail controller save method tests', function() {
      beforeEach(angular.mock.inject(function($controller){
        scope.fakeStateParams.clubId = null;
        $controller('ClubCtrl', {$scope: scope, $stateParams: scope.fakeStateParams});
       }));

      it('Submit with clubId calls post on server, post succeeds', function(){
        spyOn(scope.$state, "transitionTo").andCallThrough();

        scope.club.name = 'Changed name';     
        scope.submit();

        scope.httpBackend.expectPOST('../clubs.json').respond({});
        scope.$digest();
        scope.httpBackend.flush();
        expect(scope.$state.transitionTo).toHaveBeenCalledWith('clubs');
      });
    });
  });
});

You should get a test result that looks similar to the below (albeit with 10 tests now, I haven’t updated the screenshot):

Tutorial_4.5 test results

The final code for this test is on github: tutorial_7.

Onwards and upwards to tutorial number 8, in which we add error handling.

Advertisements

One thought on “7: Adding unit testing using karma

  1. Pingback: AngularJS and Rails 4 CRUD application using ng-boilerplate and twitter bootstrap 3: Tutorial Index | technpol

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s