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 $modals and finding yourself repeatedly stubbing out the $modalInstance to test those controllers, this is a good approach for speeding that process up!