Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing AngularUI Bootstrap modal instance controller

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.)

like image 796
David Pisoni Avatar asked Jun 23 '14 18:06

David Pisoni


2 Answers

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.

like image 185
Sunil D. Avatar answered Nov 08 '22 08:11

Sunil D.


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();
});
like image 26
yvesmancera Avatar answered Nov 08 '22 08:11

yvesmancera