AngularJS Unit Tests with Sinon.JS

[Fuente: http://sett.ociweb.com/sett/settNov2014.html]

AngularJS Unit Tests with Sinon.JS

by
Jason Schindler, Software Engineer
Object Computing, Inc. (OCI)

Introduction

AngularJS is an open-source framework for building single page web applications. It utilizes two-way data binding to dynamically keep information synchronized between the model and view layers of the application and directives as a way to extend HTML to allow developers to express views in a declarative fashion. It is primarily developed and maintained by Google. For the remainder of this article, a general working knowledge of AngularJS is helpful, but not required.

The AngularJS development team considers testing an extremely important part of the development process, and it shows. AngularJS applications are easy to unit-test due to built-in dependency injection and clear separation of roles between the view, controller, service, filter, and directive layers.

Most AngularJS projects use the Jasmine BDD-friendly testing framework along with the Karma test runner for unit testing and Protractor for end-to-end or acceptance level testing. Karma was initially developed by the AngularJS team and is capable of running tests in most any framework with community plugins. Because of their easy integration with AngularJS, I’ll be focusing on Jasmine and Karma for this article.

Sinon.JS is also an open-source framework. It provides spies, stubs, and mocks for use in JavaScript unit tests. It works with any unit testing framework and has no external dependencies. This article is only going to brush the surface of Sinon.JS capabilities. If you have not had an opportunity to use Sinon.JS yet, hopefully this will interest you enough to get started.

So what exactly are spies, stubs, and mocks?

A la hora de escribir un test unitario, tú sólo deberías preocuparte con la lógica de la unidad que está testando. Sin embargo, la mayoría de los códigos interactuan con otros módulos y sus implementaciones pueden algunas veces cruzarse en el camino de los tests. Spies , stubs y mocks son artefactos que nos proporcionan una forma de describir la formas en que la unidad que estamos testando interactua con otros módulos.En Sinon .JS:

  • Spies: Pueden envolver una función existente o ser anónimos. Un spy graba registros de todas las interacciones con una función incluyendo los argumentos de entrada , el return de la función , o si la function arroja una excepción.Los spies dejan que la función original se ejecute , lo único que hacen es espiar las llamadas.
  • StubsComo las spies , los stubs se montan sobre funciones existentes o pueden ser creados de forma anónima. También como los spies, graban todas sus interacciones. Cuando utilizas un stub, la función original no es llamada. En su lugar, le dices al stub qué es lo que te gustaría hacer cada vez que es invocada. Haciendo esto, podemos controlar el flujo de ejecución de la unidad bajo testing de forma efectiva, controlando la salida de las funciones con las cuales el código interactua.
  • MocksLos Mocks son un poco distintos de los stubs y los spies. Los Mocks crean expectations. Si le dices a un stub que siempre retorne el valor ’42’ cuando se le pasa ’12’, no se quejará si nunca es invocado con el parámetros ’12’ o si nunca es invocado. El stub esencialmente sólo sabe cómo responder cuando recibe ’12’. Las expectations difieren en que se lanzará una excepción si no se cumplen. Es decir, si tienes una expectation que dice que una función será llamada con el parámetro de ’12’ y nunca se hace , lanzará una excepción durante el paso de verificación lo que provocará que el test falle.

I mostly use stubs and anonymous spies for my tests. Anonymous spies are good for situations where the unit under test does not require a function to return any value, but I still need to assert that the call was made. Stubs are useful when I want to control the flow of execution in my test. In the examples below, I will use both anonymous spies and stubs to separate the unit under test from its dependencies.

But… Jasmine already has spies built in!

Yes it does. I have no issue with the Jasmine spies already available. I just happen to like Sinon.JS better. 🙂

Adding Sinon.JS to your AngularJS project

Let’s start by adding Karma and Sinon.JS to your project.

Add Karma dependencies to your project.

Note: If you are already using Karma with Jasmine you can skip this section, but please make sure that the version of your karma-jasmine package is 0.2.x. At the time of this writing, annpm install karma-jasmine command by default installs the 0.1.x version of karma-jasmine which is not compatible with the Sinon.JS Jasmine matchers below.

To get Karma in your project, we will be adding the karma package, one or more launchers, and karma-jasmine.

First, let’s install Karma and PhantomJS globally so that we can run them from the command line. Karma is a unit test runner that integrates nicely with AngularJS, and PhantomJS is a headless browser that is used for web application testing.

npm install -g karma phantomjs

Now we can install the karma-jasmine package, and one or more launchers.

npm install --save-dev karma-phantomjs-launcher karma-jasmine@0.2.x

This only installs the PhantomJS launcher. You may wish to install additional launchers for other browsers on your machine. Other options include: karma-firefox-launcher, karma-chrome-launcher, and karma-ie-launcher. For a more complete list, visit npmjs.org. I tend to run unit tests almost exclusively in PhantomJS to take advantage of the speedy execution time. If you start encountering odd errors (For example: not being able to use Function.prototype.bind) it helps to run your tests using another browser to verify that PhantomJS is behaving correctly.

Note: The PhantomJS bug listed above can be overcome by using es5-shim.

Create a Karma configuration file

The easiest way to create a Karma configuration file, is to run karma init from your project folder. This will ask a number of questions about your project and generate a configuration file based on your answers. After the command has completed, a file named karma.conf.js should be available in your project. Mine looks 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
module.exports = function(config){
  config.set({
    basePath: '',
    frameworks: ['jasmine'],
    files: [
      'bower_components/angular/angular.js',
      'bower_components/angular-mocks/angular-mocks.js',
      'bower_components/sinonjs/sinon.js',
      'bower_components/jasmine-sinon/lib/jasmine-sinon.js',
      'app/app.js',
      'app/**/*.js',
      'app/**/*.test.js'
    ],
    exclude: [],
    preprocessors: {},
    reporters: ['progress'],
    port: 9876,
    colors: true,
    autoWatch: false,
    //browsers: ['Firefox','PhantomJS'],
    browsers: ['PhantomJS'],
    singleRun: true
  });
};

There are a number of options available here. As long as your files array includes the appropriate source files, dependencies, and test files, and you have installed launchers for the items in your browsers array, you should be good to go.

Note: The bower_components folder in the example above is where Bower places your dependencies by default.

Getting Sinon.JS

In addition to Sinon.JS, we will also be using jasmine-sinon which adds a number of Sinon.JS matchers to Jasmine for us.

If you are using Bower, getting Sinon.JS in your project is as simple as:

bower install --save-dev sinonjs jasmine-sinon

If you are not, please visit the links above and pull down the JavaScript files needed and place them within your project.

Once Sinon.JS and jasmine-sinon are available, make sure they are loaded in the files array of your karma.conf.js. An example of this is provided above.

Incorporating into your build

Karma plugins are available for Grunt and Gulp. If you aren’t using a build system, you can run your unit tests by executing karma start in your project folder.

Useful AngularJS/Sinon.JS recipes

Great! You should now have Sinon.JS available to your AngularJS project. Let’s go through a few ways to Sinon-ify our tests.

Use anonymous spies (or stubs) instead of NOOP or short functions.

Consider the following AngularJS controller test:

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
describe('MainController', function(){
  var testController,
      testScope;
  beforeEach(function(){
    module('SinonExample');
    inject(function($rootScope, $controller){
      testScope = $rootScope.$new();
      testController = $controller('MainController', {
        $scope: testScope,
        SomeService: {
          refreshDefaults: function(){},
          registerItem: function(){},
          unRegisterItem: function(){}
        }
      });
    });
  });
  it('has default messages', function(){
    expect(testScope.helloMsg).toBe('World!');
    expect(testScope.errorMsg).toBe('');
  });
});

In this example, MainController receives $scope and SomeService dependencies. In order to properly isolate the operations of SomeService from the controller under test, I have assigned empty (or NOOP) functions to the properties in SomeService that the controller code is using. This is a sensible starting point, and correctly detaches any code in SomeService from my item under test.

So let’s complicate things a small bit. If I don’t call SomeService.refreshDefaults() before using the rest of the service, things may break. Also, I want to know that I have correctly registered MAIN with the service. Starting from the point above, the next logical step would 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
var testController,
    testScope,
    fakeSomeService;
beforeEach(function(){
  module('SinonExample');
  fakeSomeService = {
    refreshDefaultsCalled: false,
    lastRegisteredItem: 'NONE',
    refreshDefaults: function(){
      this.refreshDefaultsCalled = true;
    },
    registerItem: function(item){
      this.lastRegisteredItem = item;
    },
    unRegisterItem: function(){}
  };
  inject(function($rootScope, $controller){
    testScope = $rootScope.$new();
    testController = $controller('MainController', {
      $scope: testScope,
      SomeService: fakeSomeService
    });
  });
});
it('refreshes defaults on load', function(){
  expect(fakeSomeService.refreshDefaultsCalled).toBe(true);
});
it('registers MAIN on load', function(){
  expect(fakeSomeService.lastRegisteredItem).toBe('MAIN');
});

While completely usable, this test is starting to smell a little funny. We have created a fake version of SomeService that tracks if refreshDefaults was called and the final argument passed to registerItem. It isn’t difficult to imagine additional scenarios that muddy the water further. For example, tracking the number of times that refreshDefaults is called or the value of the 3rd item that was registered.

This is an excellent use case for anonymous spies. Sinon.JS spies will record when they are called, as well as the inputs and outputs of each call. In our case, we are using anonymous spies so tracking outputs isn’t needed.

Here are the same tests using Sinon.JS:

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
var testController,
    testScope,
    fakeSomeService;
beforeEach(function(){
  module('SinonExample');
  fakeSomeService = {
    refreshDefaults: sinon.spy(),
    registerItem: sinon.spy(),
    unRegisterItem: sinon.spy()
  };
  inject(function($rootScope, $controller){
    testScope = $rootScope.$new();
    testController = $controller('MainController', {
      $scope: testScope,
      SomeService: fakeSomeService
    });
  });
});
it('refreshes defaults on load', function(){
  expect(fakeSomeService.refreshDefaults).toHaveBeenCalled();
});
it('registers MAIN on load', function(){
  expect(fakeSomeService.registerItem).toHaveBeenCalledWith('MAIN');
});

Isn’t that better? By replacing our NOOP functions with anonymous Sinon.JS spies, we have gained the ability to glance into the calls that have occurred without writing additional code just to do so. Additionally, we can now inspect specific calls or even the order of the calls if needed:

1
expect(fakeSomeService.registerItem).toHaveBeenCalledAfter(fakeSomeService.refreshDefaults);

If you want to switch out all current (and future) functions on a AngularJS service, there is an additional step you can take. You can use sinon.stub(serviceInstance) to switch all service functions with stubs. Because stubs do not call through to the original function and because they are also spies, we can get the same functionality at the anonymous spies above by stubbing the entire service. For example:

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
var testController,
    testScope,
    stubbedSomeService;
beforeEach(function(){
  module('SinonExample');
  inject(function($rootScope, $controller, SomeService){
    testScope = $rootScope.$new();
    stubbedSomeService = sinon.stub(SomeService);
    testController = $controller('MainController', {
      $scope: testScope,
      SomeService: stubbedSomeService
    });
  });
});
it('refreshes defaults on load', function(){
  expect(stubbedSomeService.refreshDefaults).toHaveBeenCalled();
});
it('registers MAIN on load', function(){
  expect(stubbedSomeService.registerItem).toHaveBeenCalledWith('MAIN');
  expect(stubbedSomeService.registerItem).toHaveBeenCalledAfter(stubbedSomeService.refreshDefaults);
});

By injecting an instance of SomeService in our beforeEach function, we were able to stub all of the functions available to service consumers with one call. Because stubs do not call through to the original methods and are also spies, the functionality of our test doesn’t change.

Warning: Using this method to stub an entire service should only be used when you have a very good understanding of what functionality the service provides. It is usually best to only do this with services that you have written as part of your application. Also, please remember that stubs only work with functions. If you are storing strings or other non-function values as properties on your service, those will remain unchanged.