How to test Angular $modal using Sinon.js? How to test Angular $modal using Sinon.js? angularjs angularjs

How to test Angular $modal using Sinon.js?


This is going to be a long answer, touching on unit testing, stubbing and sinon.js(to some extent).

(If you would like to skip ahead, scroll down to after the #3 heading and have a look at the final implementation of your spec)

1. Establish the goal

I want to verify that the user getting passed in is the same one used in the resolve parameter of the modal.

Great, so we have a goal.

The return value of $modal.open's resolve { user: fn }, is expected to be the user we passed into the $scope.showProfile method.

Given that $modal is a external dependency in your implementation, we simply do not care about the internal implementation of $modal. Obviously we do not want to inject the real $modal service into our test suite.

Having looked at your test suite, you seem to have a grip on that already (sweet!) so we won't have to touch on the reasoning behind that too much.

I suppose the initial wording of the expectation would be aching to something like:

$modal.open should have been invoked, and its resolve.user function should return the user passed to $scope.showProfile.

2. Prep

I'm going to cut out a lot of the stuff from your test suite now, so as to make it ever so slightly more readable. If there are parts missing that are vital to the spec passing, I apologise.

beforeEach

I would start off by simplifying the beforeEach block. It is way cleaner to have a single beforeEach block per describe block, it eases up on readability and reduces boilerplate code.

Your simplified beforeEach block could look something like this:

var $scope, $modal, createController; // [1]: createController(?)beforeEach(function () {  $modal = {}; // [2]: empty object?   module('myApp', function ($provide) {    $provide.value('$modal', $modal); // [3]: uh?   });  inject(function ($controller, $injector) { // [4]: $injector?     $scope = $injector.get('$rootScope').$new();    $modal = $injector.get('$modal');    createController = function () { // [5(1)]: createController?!      return $controller('profileController', {        $scope: $scope        $modal: $modal      });    };  });  // Mock API's  $modal.open = sinon.stub(); // [6]: sinon.stub()? });

So, some notes on what I've added/changed:

[1]: createController is something we've established at my company for quite some time now when writing unit tests for angular controllers. It gives you a lot of flexibility in modifying said controllers dependencies on a per spec basis.

Suppose you had the following in your controller implementation:

.controller('...', function (someDependency) {  if (!someDependency) {    throw new Error('My super important dependency is missing!');    }  someDependency.doSomething();});

If you wanted to write a test for the throw, but you passed up on the createController method - you would need to setup a separate describe block with it's own beforeEach|before call to set someDependency = undefined. Major hassle!

With a "delayed $inject", it's as simple as:

it('throws', function () {  someDependency = undefined;  function fn () {    createController();  }  expect(fn).to.throw(/dependency missing/i);});

[2]: empty object By overwriting the global variable with an empty object at the start of your beforeEach block, we can be certain that any leftover methods from the previous spec is dead.


[3]: $provide By $providing the mocked out (at this point, empty) object as a value to our module, we don't have to load up the module containing the real implementation of $modal.

In essence, this makes unit testing angular code a breeze, as you will never run into the Error: $injector:unpr Unknown Provider in your unit tests again, by simply killing any and all references to un-interesting code for the nimble, focused unit test.


[4]: $injector I prefer to use the $injector, as it reduces the amount of arguments that you need to supply to the inject() method to almost nothing. Do as you please here!


[5]: createController Read #1.


[6]: sinon.stub At the end of your beforeEach block is where I would suggest you supply all of your stubbed out dependencies with the necessary methods. Stubbed out methods.

If you are positive that a stubbed out method will and should always return, say a resolved promise - you could change this line to:

dependency.mockedFn = sinon.stub().returns($q.when());// dont forget to expose, and $inject -> $q!

But, in general I would recomment explicit return statements in the individual it()'s.

3. Writing the spec

OK, so to go back to the problem at hand.

Given the aforementioned beforeEach block, your describe/it could look something like this:

describe('displaying a user profile', function () {  it('matches the passed in user details', function () {    createController();  });});

One would think we need the following:

  • A user object.
  • A call to $scope.showProfile.
  • An expectation on the return value of the resolve function of the invoked $modal.open.

The problem with that is the notion of testing something that is out of our hands. What $modal.open() does behind the scenes is not in the scope of the spec suite for your controller - it is a dependency, and dependencies get stubbed out.

We can however test that our controller invoked $modal.open with the correct params, but the relation between resolve and controller is not a pat of this spec suite (more on that later).

So to revise our needs:

  • A user object.
  • A call to $scope.showProfile.
  • An expectation on the parameters passed to $modal.open.

it('calls $modal.open with the correct params', function ({  // Preparation  var user = { name: 'test' };  var expected = {    templateUrl: 'components/profile/profile.html',    resolve: {      user: sinon.match(function (value) {        return value() === user;      }, 'boo!')    },    controller: sinon.match.any          };  // Execution  createController();  $scope.showProfile(user);  // Expectation  expect($modal.open).to.have    .been.calledOnce    .and.calledWithMatch(expected);});

I want to verify that the user getting passed in is the same one used in the resolve parameter of the modal.

"$modal.open should have been instantiated, and its resolve.user function should return the user passed to $scope.showProfile."

I would say our spec covers exactly that - and we've 'cancelled out' $modal to boot. Sweet.

An explanation of custom matchers taken from the sinonjs docs.

Custom matchers are created with the sinon.match factory which takes a test function and an optional message. The test function takes a value as the only argument, returns true if the value matches the expectation and false otherwise. The message string is used to generate the error message in case the value does not match the expectation.

In essence;

sinon.match(function (value) {  return /* expectation on the behaviour/nature of value */}, 'optional_message');

If you absolutely wanted to test the return value of the resolve (the value that ends up in the $modal controller), I would suggest you test the controller in isolation by extracting it to a named controller, rather than an anonymous function.

$modal.open({  // controller: function () {},  controller: 'NamedModalController'});

This way you can write expectations for the modal controller (in another spec file, of course) as such:

it('exposes the resolved {user} value onto $scope', function () {  user = { name: 'Mike' };  createController();  expect($scope).to.have.property('user').that.deep.equals(user);});

Now, a lot of this was re-iteration - you are already doing a lot of what I touched upon, here's hoping I am not coming off as a tool.

Some of the preparation data in the it() I proposed could be moved to a beforeEach block - but I would suggest only doing so when there's an abundance of tests calling the same code.

Keeping a spec suite DRY isn't as important as keeping your specs explicit so as to avoid any confusion when another developer comes over to read them and fix some regression error(s).


To finalise, some of the inline comments you wrote in your original:

sinon.match.any

var modalOptions = {  resolve:{    agent:sinon.match.any // No idea if this is correct, trying to match jasmine.any(Function)  },};

If you want to match it against a function, you would do:

sinon.match.func which is the equivalent of jasmine.any(Function).

sinon.match.any matches anything.


sinon.stub.yield([arg1, arg2])

// open cannot yield since it was not yet invoked.modalSpy.yield(function(options){   actualOptions = options;  return fakeModal;});

First of all, you have multiple methods on $modal that are (or should be) stubbed out. As such, I think it's a bad idea to mask $modal.open under modalSpy - it's not very explicit about which method is to yield.

Secondly, you are mixing spy with stub (I do it all the time...), when referencing your stub as modalSpy.

A spy wraps the original functionality and leaves it be, recording all of the 'events' for upcoming expectation(s), and that's about it really.

A stub is effectively a spy, with the difference that we can alter the behaviour of said function by supplying .returns(), .throws() etc. In short; a juiced up spy.

Like the error message suggests, the function cannot yield until after it has been invoked.

  it('yield / yields', function () {    var stub = sinon.stub();    stub.yield('throwing errors!'); // will crash...    stub.yields('y');    stub(function () {      console.log(arguments);    });    stub.yield('x');    stub.yields('ohno'); // wont happen...  });

If we were to remove the stub.yield('throwing errors!'); line from this spec, the output would look like so:

LOG: Object{0: 'y'}LOG: Object{0: 'x'}

Short and sweet (that's about as much as I know in regards to yield/yields);

  • yield after the invokation of your stub/spy callback.
  • yields before the invokation of your stub/spy callback.

If you've reached this far you've probably realised that I could ramble on and on about this subject for hours on end. Luckily I'm getting tired and it is time for some shut eye.


Some resources loosely related to the subject: