26

This is a somewhat of a follow-on question to this one: Mocking $modal in AngularJS unit tests

The referenced SO is an excellent question with very useful answer. The question I am left with after this however is this: how do I unit test the modal instance controller? In the referenced SO, the invoking controller is tested, but the modal instance controller is mocked. Arguably the latter should also be tested, but this has proven to be very tricky. Here's why:

I'll copy the same example from the referenced SO here:

.controller('ModalInstanceCtrl', function($scope, $modalInstance, items){
  $scope.items = items;
  $scope.selected = {
    item: $scope.items[0]
  };

  $scope.ok = function () {
    $modalInstance.close($scope.selected.item);
  };

  $scope.cancel = function () {
    $modalInstance.dismiss('cancel');
  };
});

So my first thought was that I would just instantiate the controller directly in a test, just like any other controller under test:

beforeEach(inject(function($rootScope) {
  scope = $rootScope.$new();
  ctrl = $controller('ModalInstanceCtrl', {$scope: scope});
});

This does not work because in this context, angular does not have a provider to inject $modalInstance, since that is supplied by the UI modal.

Next, I turn to plan B: use $modal.open to instantiate the controller. This will run as expected:

beforeEach(inject(function($rootScope, $modal) {
  scope = $rootScope.$new();
  modalInstance = $modal.open({
    template: '<html></html>',
    controller: 'ModalInstanceCtrl',
    scope: scope
  });
});

(Note, template can't be an empty string or it will fail cryptically.)

The problem now is that I have no visibility into the scope, which is the customary way to unit test resource gathering, etc. In my real code, the controller calls a resource service to populate a list of choices; my attempt to test this sets an expectGet to satisfy the service my controller is using, and I want to validate that the controller is putting the result in its scope. But the modal is creating a new scope for the modal instance controller (using the scope I pass in as a prototype), and I can't figure out how I can get a hole of that scope. The modalInstance object does not have a window into the controller.

Any suggestions on the "right" way to test this?

(N.B.: the behavior of creating a derivative scope for the modal instance controller is not unexpected – it is documented behavior. My question of how to test it is still valid regardless.)

Community
  • 1
  • 1
David Pisoni
  • 3,317
  • 2
  • 25
  • 35

4 Answers4

31

I test the controllers used in modal dialogs by instantiating the controller directly (the same way you initially thought to do it above).

Since there there's no mocked version of $modalInstance, I simply create a mock object and pass that into the controller.

var modalInstance = { close: function() {}, dismiss: function() {} };
var items = []; // whatever...

beforeEach(inject(function($rootScope) {
  scope = $rootScope.$new();
  ctrl = $controller('ModalInstanceCtrl', {
      $scope: scope, 
      $modalInstance: modalInstance, 
      items: items
  });
}));

Now the dependencies for the controller are satisfied and you can test this controller like any other controller.

For example, I can do spyOn(modalInstance, 'close') and then assert that my controller is closing the dialog at the appropriate time.

isherwood
  • 58,414
  • 16
  • 114
  • 157
Sunil D.
  • 17,983
  • 6
  • 53
  • 65
13

Alternatively, if you're using jasmine, you can mock the $uibModalInstance using the createSpy method:

beforeEach(inject(function ($controller, $rootScope) {
  $scope = $rootScope.$new();
  $uibModalInstance = jasmine.createSpyObj('$uibModalInstance', ['close', 'dismiss']);

  ModalCtrl = $controller('ModalCtrl', {
    $scope: $scope,
    $uibModalInstance: $uibModalInstance,
  });
}));

And test it without having to call spyOn on each method, let's say you have 2 scope methods, cancel() and confirm():

it('should let the user dismiss the modal', function () {
  expect($scope.cancel).toBeDefined();
  $scope.cancel();
  expect($uibModalInstance.dismiss).toHaveBeenCalled();
});

it('should let the user confirm the modal', function () {
  expect($scope.confirm).toBeDefined();
  $scope.confirm();
  expect($uibModalInstance.close).toHaveBeenCalled();
});
yvesmancera
  • 2,915
  • 5
  • 24
  • 33
  • 1
    Even though Sunil D's answer definitely works I feel this should definitely be the accepted answer for most implementations since you will most likely want to spyOn them. If you don't want that then Sunil D's works splendid. – perry Feb 03 '16 at 05:18
  • 1
    Plus one. I agree, I find myself creating the spies this way for mock objects, instead of what I did above. – Sunil D. Feb 17 '16 at 18:15
  • 1
    @perry Thank you! But the question isn't tagged as jasmine or even mentioned, so Sunil's answer works for any testing framework while mine works only if you're using jasmine. – yvesmancera Feb 17 '16 at 18:41
  • 1
    @yvesmancera This is true. Guess I missed that. So into Jasmine at the moment that when I see Angular testing my mind just goes there :D – perry Feb 25 '16 at 00:06
  • @perry FWIW: I'm using the Sunil's approach in the selected answer and spyOn is working fine with it. – JeffryHouser Dec 08 '16 at 23:52
  • @JeffryHouser Sorry, didn't mean you can't spy on them. Meant that his approach creates the spy directly without creating a mock first and then a spy. Skips one step :) – perry Dec 14 '16 at 00:04
0

The same problem is with $uidModalInstance and you can solve it in similar way:

var uidModalInstance = { close: function() {}, dismiss: function() {} };

$ctrl = $controller('ModalInstanceCtrl', {
   $scope: $scope,
   $uibModalInstance: uidModalInstance
});

or as said @yvesmancera you can use jasmine.createSpy method instead, like:

var uidModalInstance = jasmine.createSpyObj('$uibModalInstance', ['close', 'dismiss']);

$ctrl = $controller('ModalInstanceCtrl', {
   $scope: $scope,
   $uibModalInstance: uidModalInstance
});
kris_IV
  • 2,396
  • 21
  • 42
0

Follow below given steps:

  • Define stub for ModalInstance like give below

            uibModalInstanceStub = {
                close: sinon.stub(),
                dismiss: sinon.stub()
            };
    
  • Pass the modal instance stub while creating controller

        function createController() {
            return $controller(
                ppcConfirmGapModalComponentFullName,
                {
                    $scope: scopeStub,
                    $uibModalInstance: uibModalInstanceStub
                });
        }
    });
    
  • Stub methods close(), dismiss() will get called as part of the tests

    it('confirm modal - verify confirm action, on ok() call calls modalInstance close() function', function() { action = 'Ok'; scopeStub.item = testItem; createController(); scopeStub.ok(); });

Dilip Nannaware
  • 1,410
  • 1
  • 16
  • 24