Angular Unit Testing part 2 - $modal
I've been working a lot with Angular UI in a current project and have utilized their $modal functionality a bit. I found myself writing unit tests for controllers used with the modals, and so I wrote a test helper for this purpose.
In a prior post I went over one strategy I use for creating unit test helpers. I'll be employing that same strategy here so I won't be repeating the steps for getting started. If you need help with setting up your test helpers, please refer to that post.
Create The Helper###
At the root of the project, have a directory name test-helpers where our test helpers go. We're going to write a helper that will generate a mock of the $modalInstance
object with spies included - this is passed into the modal's controller, but is also accessible by the code that generates the modal. For instance, if I created a modal like this:
/*modalInstance gets injected into the controller MyModalCtrl
it's an object shared between the two scopes
(this current scope, and the scope of the modal's controller)
*/
var modalInstance = $modal.open({
templateUrl: 'path/to/template.html',
controller: 'MyModalCtrl'
});
Then I can get a handle to that modalInstance object injected into MyModalCtrl
like this:
angular.module('app.core').controller('MyModalCtrl', function($scope, $modalInstance) {
//$modalInstance is a reference to the same 'modalInstance' object that was returned by calling $modal.open({...}) in the snippet above
});
Since our unit test is to test the modal's controller, we'll just create our own version of $modalInstance
to pass in when instantiating the controller during the test. Let's code up the helper!
(function(self) {
/**
* @desc - Creates a mock of the $modalInstance object that the $modal service would typically use when creating a $modal so that we can intercept and spy on methods.
* @returns {close: function, dismiss: function, result: {then: function, catch: function}}
* @remarks - close and dismiss are jasmine spies.
* result emulates a promise, so the 'then' and 'catch' are spies that accept function callbacks
if 'then' and 'catch' callbacks provided, they will be called by 'close' and 'dimsiss', respectively
*/
self.mockModalInstance = function() {
return {
close: jasmine.createSpy('modalInstance.close').andCallFake(function (data) {
if(this.result.confirmCB && typeof this.result.confirmCB === 'function') {
this.result.confirmCB(data);
}
}),
dismiss: jasmine.createSpy('modalInstance.dismiss').andCallFake(function (reason) {
if(this.result.cancelCB && typeof this.result.cancelCB === 'function') {
this.result.cancelCB(reason);
}
}),
result: {
then: jasmine.createSpy('modalInstance.result.then').andCallFake(function (confirm, cancel) {
this.confirmCB = confirm || this.confirmCB;
this.cancelCB = cancel || this.cancelCB;
}),
catch: jasmine.createSpy('modalInstance.result.catch').andCallFake(function (cb) {
this.cancelCB = cb || this.cancelCB;
})
}
};
};
}((module || {}).exports || window));
Use the helper in our Test###
Now that we have our helper, we just need to write up a test for our controller, and use the helper. Here's the the test file, named MyModalCtrl-spec.js:
describe('MyModalCtrl Functionality', function(){
beforeEach(module('app.core'));
//variables accessible for the entire suite of tests
var scope, controller, modalInstance;
//inject the items we need to populate the variables
beforeEach(inject(function($rootScope, $controller){
scope = $rootScope.$new(); //get a new scope
modalInstance = window.mockModalInstance(); //Using our test helper ;-)
//Here, we make the controller and pass in the $modalInstance dependency
//$controller taks in name as first parameter, and object hash of resolved dependencies as second parameter.
controller = $controller('MyModalCtrl', {
$scope: scope,
$modalInstance: modalInstance
});
}));
//Quick test - let's say that there's a method on the controller called 'close' that invokes $modalInstance.close($scope.result)
it('should call the modalInstance.close method on $scope.close', function(){
scope.result = {name: 'Test Result'};
//We can also add a callback to the modalInstance.result (which is a promise)
var callback = jasmine.createSpy('success.callback');
modalInstance.result.then(callback);
//calling scope.close should call close on the scope's $modalInstance reference, and $modalInstance should invoke the appropriate callback on close
scope.close();
expect(modalInstance.close).toHaveBeenCalledWith(scope.result);
//callback should've also been called
expect(callback).toHaveBeenCalledWith(scope.result);
});
});
And it's that simple! Our helper keeps us from having to mock out the $modalInstance
on every test spec file, and it also comes with spies so that we can track everything about it. If you're building an app with multiple $modal
s and finding yourself repeatedly stubbing out the $modalInstance
to test those controllers, this is a good approach for speeding that process up!