6/04/2014

Angular and Jasmine: Injecting into the test environment

UPDATE: The team at Gaikai, where I work, has released an open source testing framework for Angular which streamlines this process and greatly simplifies the testing process. I wrote about it here: http://randomjavascript.blogspot.com/2015/01/ngtestharness-strap-in-with-this-new.html

I have set my sights on improving my Angular proficiency, however I am not ready to give up on all my other tools. I have a strong preference for Jasmine, and the recent release of 2.0 further cemented that comfort zone. Jasmine 2.0 made major improvements by removing window dependency and improving async support. Using Jasmine and Jasmine-Node together allows me to write tests in the same language for both server and client. I prefer to standardize as much as possible on a project, and having one language for tests on both application tiers is valuable to me in time and effort saved.

Angular is well written and encourages testing as a natural part of the development process. This makes working with a BDD/TDD/*DD development with a library like Jasmine a theoretically easy task. Unfortunately, when just starting out this may not be the case. Angular requires a very specific environment in which to function. The result of that can be a significant amount of boilerplate when creating tests for Angular applications in Jasmine. The good news is that the amount of redundancy can be minimized with careful test organization.

We will want to declare some variables as high as possible to help minimize that boilerplate. We don't want to risk polluting the global namespace even though these are tests. The compromise is fairly easy. We will accomplish both goals by creating embedded suites. The spec file will have one describe that contains that spec's entire content. Inner describe clauses will be used to manage and break up the tests. Inside of the main over-arching describe we will add variables to contain the services that all the clauses will need. The first few lines of our spec should resemble this:

describe('Test the note-editor', function () {
 var $httpBackend,
 $compile,
 $rootScope,
 $controller;

 describe('Creates the editor div', function () {

We are going to inject those services in the first describe clause via Angular's inject function. This approach will make sure that none of the code outside this suite is affected, and nothing will linger and potentially pollute other tests. We will use the beforeEach function in Jasmine to make sure the services load before any tests are started. However, there is one slight wrinkle. For this to work we need to call the Angular inject function and then set our variables in the callback. The result is something like this:

beforeEach(inject(function ($injector) {
 $compile = $injector.get('$compile');
 $rootScope = $injector.get('$rootScope');
 $httpBackend = $injector.get('$httpBackend');
 $controller = $injector.get('$controller');
}));

There is a small modification required if you are using Angular's module functionality to modularize your code. We will need to load the module into the testing environment first.

beforeEach(function () {
    module('noteEditor');
    inject(function ($injector) {
         $compile = $injector.get('$compile');
         $rootScope = $injector.get('$rootScope');
         $httpBackend = $injector.get('$httpBackend');
      $controller = $injector.get('$controller');
    });
});

Our testing environment should be ready now, so let's make our first suite for testing code.

describe('Controller sends and receives notes', function () {

One of the things Angular excels at is scope management. So we will need to create a scope to pass into our test. We will recreate the scope with each test to avoid the tests affecting each other. We will also instantiate all objects cleanly for each test. This is easily accomplished by using a beforeEach function in the inner suite as well. The scope is generated by using the $rootScope service. I am including the $httpBackEnd service for AJAX testing which will come up shortly.

var $scope,
    cntrl;

beforeEach(inject(function ($injector) {
    $scope = $rootScope.$new();
    cntrl = $controller('NoteEditorCntrl', {
        '$scope': $scope,
 '$httpBackend': $httpBackend
    });
}));

Finally, we can get to the test. This test is to make sure the controller being tested makes the expected AJAX call to the server. The $httpBackend service will intercept that AJAX call. The $httpBackend prevents specified calls from being made outside the client. The surprise here is that the $hhtpBackend service is not asynchronous. The $httpBackend service responds without a network call and does not do any IO, so the JavaScript thread never spins off a separate process for an asynchronous operation. Therefore, the operation is actually synchronous. The usual inclusion of the done function for asynchronous operations in Jasmine is not needed.*

it('Controller makes the correct REST call and calls the callback', function () {
    $httpBackend.expectGET ('notes/All').respond(200, {emptyObject:"empty"});
    cntrl.loadAll(function (e, data){
        expect(e).toBe(null);
        expect(data.emptyObject).toEqual("empty");
    });
    $httpBackend.flush();
});

The $httpBackend service is it's own beast, and I would recommend checking the documentation on it. The first $httpBackend call tells the service to listen for a GET request from the client going to the 'notes/All' url. Upon interception, the service is told to respond with a status code of 200, and a JSON string.

The loadAll function of the controller being tested is called which will issue the GET request we are expecting. The expect statements to finish the test are inside the callback so we need the http call to complete for our tests to finish. The $httpBackend service will hold on to all intercepted requests until it gets instructions to release them. The flush function is that instruction, and tells the $httpBackend service to handle all captured calls as previously configured. The callback function is called and our test completes.

The full test and code can be seen in an angular app I am working on as time allow. The url is https://c9.io/dposin/angular_note_taker. The github repo is at https://github.com/Lastalas/NoteTaker_Angular_Node.

As I said at the beginning, I am in the process of improving my understanding of Angular, and I am open to any suggestions sent my way. Angular is a very powerful framework and can be a great tool for the right job, so I recommend every JavaScript engineer become familiar with it. Although, that suggestion isn't limited to Angular, I think every JavaScript engineer should become familiar with as many different frameworks as possible. Thankfully, working in Angular does not mean giving up on all your favorite libraries and tools as shown here. So, if you are a Jasmine fan as I am, this post will hopefully assist you in using the two together successfully.


* Here is the loadAll function for reference:

    loadAll: function (fn) {
        $http.get('notes/All')
 .success(function (data, status) {
            try{
                fn(null, data);
     }catch (e){
  console.log (e)
  fn(e);
     }
 })
 .error(function (e){
            fn(e);
 })
}