AngularJS Corner – The ngMock and ngMockE2E libraries

by
Tags: , , , ,
Category:

AngularJS provides two powerful mock and test support modules, both contained in a single script file. The modules, ngMock and ngMockE2E, provide different features for different purposes, though both modules are defined in, angular-mocks.js. This led me to a bit of confusion in explaining how they work (as the docs erroneously pointed out the location of the ngMockE2E library until very recently). In this post I’ll explain the differences between the modules and how they get mounted in a test scenario.

ngMock

The ngMock module is a powerful test mock provider, which is used when unit testing your code with Jasmine.  It provides a few very useful functions and features, documented at ngMock. Here are a few highlights:

  • angular.mock.module – this function loads your module into a test, much the same way ng-app loads your module into a page. It has an alias globally for the testing engine, just called module. Typically you’ll call it in a beforeEach function:
      describe('myService tests', function() {
        // load my module, ng, and ngMock
        beforeEach(module('myApp'));
        it(...)
      });
    
  • angular.mock.inject – since we don’t run unit tests in an actual application page, we have to inject our functions into test methods by prefixing our function calls with inject.  This runs our code through the Angular injector before executing the code.  This also can be called in beforeEach, but is also called in an it (jasmine test) method when injecting services that don’t need much setup. Here is an annotated sample with beforeEach:
    describe('A test suite', function() {
      // A word on the inject function syntax...
      // Using underscores in the symbol names, such as
      // _myService_, allows you to use actual component names
      // in your tests and assign them to your
      // describe-scoped variable naturally. Angular
      // strips the underscores when searching
      // so your parameter name does not clash with a variable
      // in the describe block
       // Hold on to my service
       var myService;
       // first, establish our module
       beforeEach(module('myApp'));
       beforeEach(inject(function(_myService_) {
         myService = _myService;
       });
       it('does something to myService', function() {
         var result = myService.foo();
         expect(result).toBe(true);
       });  // end - it
    });  // end - describe
    
  • A fake logger – during unit tests, the $log service, which normally writes to the console, actually buffers into memory so you can assert whether activities were fired.  For example:
      it('calls a service which uses $log',
         inject(function(myService) {
         // assume your code writes 'hi' to the debug log
         myService.doSomething();
      }));
       afterEach(inject(function($log) {
         // first array element - the $log statement
         // second array element - each parameter to
         expect($log.debug.logs[0][0]).toBe('hi'));
       }));
    
  • A fake HTTP backend – this backend is awesome, and can be used to simulate a live $http service call response during a unit test, even asserting if the call didn’t get made properly:
    describe('my spec', function() {
      var $httpBackend;
      beforeEach(inject(function(_$httpBackend_) {
        $httpBackend = _httpBackend_;
        // expect - will fail if not performed, see afterEach below
        // you can also use whenGet if you want it to provide
        // the network call but expect it not to fail without it
        $httpBackend.expectGet('/api/customers')
          .respond([
             { id: 2, balance: 234 },
             { id: 23, balance: 444 }
          ]);
      }));
      it('does some networking in a service call', inject(
         function(myService) {
           var customers = myService.getCustomers();
           // force the fake backend to respond to the $http get call
           // as if it was real
           $httpBackend.flush();
           expect(customers).toBeDefined();
           expect(customers.length).toBe(2);
      }));
      afterEach(function() {
         // this fails the test if any methods were not
         // flushed to the $http API
         $httpBackend.verifyNoOutstandingRequest();
         // this fails the test if you fail to call the
         // $http API with one of your expected URLs
         $httpBackend.verifyNoOutstandingExpectation();
      });
    });
    

    For more examples, consult the at online documentation for $httpBackend.

There are also replacement objects for $interval, $rootElement and a few others. Here is the block of code that defines the ngMock library, which you get when you run a unit test and call the module function:

angular.module('ngMock', ['ng']).provider({
  $browser: angular.mock.$BrowserProvider,
  $exceptionHandler: angular.mock.$ExceptionHandlerProvider,
  $log: angular.mock.$LogProvider,
  $interval: angular.mock.$IntervalProvider,
  $httpBackend: angular.mock.$HttpBackendProvider,
  $rootElement: angular.mock.$RootElementProvider
}).config(['$provide', function($provide) {
  $provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
  $provide.decorator('$$rAF', angular.mock.$RAFDecorator);
  $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator);
}]);

The documentation for angular.mock.module states that “[t]he ‘ng’ and ‘ngMock’ modules are automatically loaded.” This was a hangup for me, as I had thought originally that just mounting the angular-mocks.js script caused it to add the fake logs and other items. That is not the case, so even though they both share the same Javascript source file, each is a separate module with separate features. Ultimately I think the files ought to be split, but in inspecting them, it appears they both use the same mocking code, just one provides expectation mocks and the other runtime mocks expecting a real browser to exist.

The second mocking module – ngMockE2E

This module, documented at ngMockE2E, provides fake responses to HTTP calls. However, instead of providing them as unit test mocks, it dutifly returns them over and over again, much like a real server. It can also pass requests through to the real $http service, so it can be used piecemeal while waiting for your real implementations.

I call this the hidden module because it doesn’t have its own script file. Until the most recent version of the docs, the Angular guide alluded to mounting an angular-mocke2e.js script to load this module. In fact, it is contained in angular-mocks.js, and is designed to be mounted in a running app, typically while you are still developing some backend functionality.

To use this module, you mount the mock script and you use ngMockE2E as a dependency to your application. Inject $httpBackend into a module.run method to set up your mock behavior (code coloring disabled due to some regular-expression syntax issues):

angular.module('myApp', ['ngMockE2E'])
  .run(function($httpBackend) {
    $httpBackend.whenGET('/login')
      .respond({user: 'fooBarBaz', roles: ['admin', 'user']});
    // allow views directory to go to real $http
    $httpBackend.whenGET(/^\/views\//).passThrough();
    // we have a real /api/customer service, let it through
    $httpBackend.whenGET(/^/api/customers//).passThrough();
});

Now, you can mock your $http and $resource call back-ends while running what looks like a real angular application, using a fake backend. As you add real features to your actual backend, you can start adding passthroughs. Ultimately, this is a great tool for executing Protractor end-to-end tests against predictable, fake data responses.

More can be found on this end-to-end version of the $httpBackend in the ngMockE2E $httpbackend docs.

Wrap-up

It is worth brushing up on the ngMock and ngMockE2E modules in the Angular API docs, as they are the bulk of what constitutes a very strong test mocking framework. As I’ve stated above, if you run the module function in a unit test, it imports all of the objects in the ngMock module (as well as ng).

If you require the ngMockE2E module when creating your application, it brings you the mockE2E with the browser-supportable $httpBackend API.

Don’t be fooled by any instructions in the docs stating to load angular-mocke2e.js – it doesn’t exist (see the version earlier than 1.3.0-beta 17 here with the incorrect file mounting instructions). The docs have been recently updated as of 1.3.0-beta 17.