I am trying to write Unit tests for a $modal in AngularJS. The code for the modal is located in a controller as follows:
$scope.showProfile = function(user){
var modalInstance = $modal.open({
templateUrl:"components/profile/profile.html",
resolve:{
user:function(){return user;}
},
controller:function($scope,$modalInstance,user){$scope.user=user;}
});
};
The function is called on a button in an ng-repeat in the HTML as follows:
<button class='btn btn-info' showProfile(user)'>See Profile</button>
As you can see the user is passed in and used in the modal, the data is then bound to the profile partial in its HTML.
I am using Karma-Mocha along with Karma-Sinon to try to perform the unit tests but I cannot understand how to achieve this, I want to verify that the user getting passed in is the same one used in the resolve parameter of the modal.
I have seen some examples of how to do this using Jasmine but I haven't been able to convert them to mocha + sinon tests.
Here is my attempt:
The setup code:
describe('Unit: ProfileController Test Suite,', function(){
beforeEach(module('myApp'));
var $controller, modalSpy, modal, fakeModal;
fakeModal = {// Create a mock object using spies
result: {
then: function (confirmCallback, cancelCallback) {
//Store the callbacks for later when the user clicks on the OK or Cancel button of the dialog
this.confirmCallBack = confirmCallback;
this.cancelCallback = cancelCallback;
}
},
close: function (item) {
//The user clicked OK on the modal dialog, call the stored confirm callback with the selected item
this.result.confirmCallBack(item);
},
dismiss: function (type) {
//The user clicked cancel on the modal dialog, call the stored cancel callback
this.result.cancelCallback(type);
}
};
var modalOptions = {
templateUrl:"components/profile/profile.html",
resolve:{
agent:sinon.match.any //No idea if this is correct, trying to match jasmine.any(Function)
},
controller:function($scope,$modalInstance,user){$scope.user=user;}
};
var actualOptions;
beforeEach(inject(function(_$controller_, _$modal_){
// The injector unwraps the underscores (_) from around the parameter names when matching
$controller = _$controller_;
modal = _$modal_;
modalSpy = sinon.stub(modal, "open");
modalSpy.yield(function(options){ //Doesn't seem to be correct, trying to match Jasmines callFake function but get this error - open cannot yield since it was not yet invoked.
actualOptions = options;
return fakeModal;
});
}));
var $scope, controller;
beforeEach(function() {
$scope = {};
controller = $controller('profileController', {
$scope: $scope,
$modal: modal
});
});
afterEach(function () {
modal.open.restore();
});
The actual test:
describe.only('display a user profile', function () {
it('user details should match those passed in', function(){
var user= { name : "test"};
$scope.showProfile(user);
expect(modalSpy.open.calledWith(modalOptions)).to.equal(true); //Always called with empty
expect(modalSpy.open.resolve.user()).to.equal(user); //undefined error - cannot read property resolve of undefined
});
});
My test setup and actual test is based off Jasmine code I have come across and trying to convert it to Mocha + SinonJS code, I am new to both AngularJS and writing Unit Tests so I'm hoping I just need a nudge in the right direction.
Can anyone share the correct approach to take when using Mocha + SinonJS instead of Jasmine?
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)
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.
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.
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.
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:
$scope.showProfile
.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:
$scope.showProfile
.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, returnstrue
if the value matches the expectation andfalse
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).
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.
// 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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With