Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking $modal in AngularJS unit tests

I'm writing a unit test for a controller that fires up a $modal and uses the promise returned to execute some logic. I can test the parent controller that fires the $modal, but I can't for the life of me figure out how to mock a successful promise.

I've tried a number of ways, including using $q and $scope.$apply() to force the resolution of the promise. However, the closest I've gotten is putting together something similar to the last answer in this SO post;

I've seen this asked a few times with the "old" $dialog modal. I can't find much on how to do it with the "new" $dialog modal.

Some pointers would be tres appreciated.

To illustrate the problem I'm using the example provided in the UI Bootstrap docs, with some minor edits.

Controllers (Main and Modal)

'use strict';  angular.module('angularUiModalApp')     .controller('MainCtrl', function($scope, $modal, $log) {         $scope.items = ['item1', 'item2', 'item3'];          $scope.open = function() {              $scope.modalInstance = $modal.open({                 templateUrl: 'myModalContent.html',                 controller: 'ModalInstanceCtrl',                 resolve: {                     items: function() {                         return $scope.items;                     }                 }             });              $scope.modalInstance.result.then(function(selectedItem) {                 $scope.selected = selectedItem;             }, function() {                 $log.info('Modal dismissed at: ' + new Date());             });         };     })     .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');         };     }); 

The view (main.html)

<div ng-controller="MainCtrl">     <script type="text/ng-template" id="myModalContent.html">         <div class="modal-header">             <h3>I is a modal!</h3>         </div>         <div class="modal-body">             <ul>                 <li ng-repeat="item in items">                     <a ng-click="selected.item = item">{{ item }}</a>                 </li>             </ul>             Selected: <b>{{ selected.item }}</b>         </div>         <div class="modal-footer">             <button class="btn btn-primary" ng-click="ok()">OK</button>             <button class="btn btn-warning" ng-click="cancel()">Cancel</button>         </div>     </script>      <button class="btn btn-default" ng-click="open()">Open me!</button>     <div ng-show="selected">Selection from a modal: {{ selected }}</div> </div> 

The test

'use strict';  describe('Controller: MainCtrl', function() {      // load the controller's module     beforeEach(module('angularUiModalApp'));      var MainCtrl,         scope;      var fakeModal = {         open: function() {             return {                 result: {                     then: function(callback) {                         callback("item1");                     }                 }             };         }     };      beforeEach(inject(function($modal) {         spyOn($modal, 'open').andReturn(fakeModal);     }));       // Initialize the controller and a mock scope     beforeEach(inject(function($controller, $rootScope, _$modal_) {         scope = $rootScope.$new();         MainCtrl = $controller('MainCtrl', {             $scope: scope,             $modal: _$modal_         });     }));      it('should show success when modal login returns success response', function() {         expect(scope.items).toEqual(['item1', 'item2', 'item3']);          // Mock out the modal closing, resolving with a selected item, say 1         scope.open(); // Open the modal         scope.modalInstance.close('item1');         expect(scope.selected).toEqual('item1');          // No dice (scope.selected) is not defined according to Jasmine.     }); }); 
like image 742
coderigo Avatar asked Jan 19 '14 08:01

coderigo


People also ask

What is mocking in unit testing Angular?

Introduction. Mocking is a great idea for testing Angular apps because it makes maintenance easier and helps reduce future bugs. There are a few complex tools, such as XUnit, for mocking an Angular CLI project. You can execute the mocking methods described in this guide only if you use vanilla Jasmine + Angular Testbed ...

How do you mock a component in Angular unit testing?

A mock component in Angular tests can be created by MockComponent function. The mock component respects the interface of its original component, but all its methods are dummies. To create a mock component, simply pass its class into MockComponent function.

What can be mocked for unit testing?

What is mocking? Mocking is a process used in unit testing when the unit being tested has external dependencies. The purpose of mocking is to isolate and focus on the code being tested and not on the behavior or state of external dependencies.

Should unit test mock dependencies?

Correct. You should mock things that depend on anything persistent or external in order to prevent the test from depending on anything persistent or external.


1 Answers

When you spy on the $modal.open function in the beforeEach,

spyOn($modal, 'open').andReturn(fakeModal);  or   spyOn($modal, 'open').and.returnValue(fakeModal); //For Jasmine 2.0+ 

you need to return a mock of what $modal.open normally returns, not a mock of $modal, which doesn’t include an open function as you laid out in your fakeModal mock. The fake modal must have a result object that contains a then function to store the callbacks (to be called when the OK or Cancel buttons are clicked on). It also needs a close function (simulating an OK button click on the modal) and a dismiss function (simulating a Cancel button click on the modal). The close and dismiss functions call the necessary call back functions when called.

Change the fakeModal to the following and the unit test will pass:

var fakeModal = {     result: {         then: function(confirmCallback, cancelCallback) {             //Store the callbacks for later when the user clicks on the OK or Cancel button of the dialog             this.confirmCallBack = confirmCallback;             this.cancelCallback = cancelCallback;         }     },     close: function( item ) {         //The user clicked OK on the modal dialog, call the stored confirm callback with the selected item         this.result.confirmCallBack( item );     },     dismiss: function( type ) {         //The user clicked cancel on the modal dialog, call the stored cancel callback         this.result.cancelCallback( type );     } }; 

Additionally, you can test the cancel dialog case by adding a property to test in the cancel handler, in this case $scope.canceled:

$scope.modalInstance.result.then(function (selectedItem) {     $scope.selected = selectedItem; }, function () {     $scope.canceled = true; //Mark the modal as canceled     $log.info('Modal dismissed at: ' + new Date()); }); 

Once the cancel flag is set, the unit test will look something like this:

it("should cancel the dialog when dismiss is called, and $scope.canceled should be true", function () {     expect( scope.canceled ).toBeUndefined();      scope.open(); // Open the modal     scope.modalInstance.dismiss( "cancel" ); //Call dismiss (simulating clicking the cancel button on the modal)     expect( scope.canceled ).toBe( true ); }); 
like image 89
Brant Avatar answered Sep 18 '22 15:09

Brant