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!

Loading Google+ Comments ...