在AngularJSunit testing中嘲讽$ modal

我正在为一个控制器写一个unit testing,触发$modal并使用返回的promise来执行一些逻辑。 我可以testing启动$ modal的父控制器,但是我不能为了我的生活找出如何嘲笑一个成功的承诺。

我尝试了很多方法,包括使用$q$scope.$apply()来强制承诺的解决scheme。 然而,我最近得到的是在这个 SOpost中的最后一个答案类似的东西,

我已经看到这个问题几次与“旧” $dialog模式。 我无法find如何使用“新” $dialog模式做到这一点。

有些指针会被赞赏。

为了说明这个问题,我使用了UI Bootstrap文档中提供的示例 ,并进行了一些小修改。

控制器(主和模式)

 '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'); }; }); 

视图(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> 

考试

 '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. }); }); 

当你窥探beforeEach中的$ modal.open函数时,

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

你需要返回一个$ modal.open通常返回的模拟,而不是$ modal的模拟,它不包含你在fakeModal模拟中展开的一个open函数。 假模式必须有一个result对象,其中包含一个then函数来存储callback(当点击OK或Cancelbutton时被调用)。 它还需要一个closefunction(模拟点击模式上的确定button)和dismissfunction(模拟点击模式上的取消button)。 closedismiss函数在调用时调用必要的callback函数。

fakeModal更改为以下,unit testing将通过:

 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 ); } }; 

另外,你可以通过在取消处理程序中添加一个属性来testing取消对话框的情况,在这种情况下$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()); }); 

一旦取消标志被设置,unit testing将如下所示:

 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 ); }); 

要添加到Brant的答案,这是一个稍微改进的模拟,可以让你处理一些其他情况。

 var fakeModal = { result: { then: function (confirmCallback, cancelCallback) { this.confirmCallBack = confirmCallback; this.cancelCallback = cancelCallback; return this; }, catch: function (cancelCallback) { this.cancelCallback = cancelCallback; return this; }, finally: function (finallyCallback) { this.finallyCallback = finallyCallback; return this; } }, close: function (item) { this.result.confirmCallBack(item); }, dismiss: function (item) { this.result.cancelCallback(item); }, finally: function () { this.result.finallyCallback(); } }; 

这将允许模拟处理的情况下…

您可以使用.catch() .finally() .catch().finally()处理程序样式的模式,而不是将2个函数( successCallback, errorCallback )传递给successCallback, errorCallback .then() ,例如:

 modalInstance .result .then(function () { // close hander }) .catch(function () { // dismiss handler }) .finally(function () { // finally handler }); 

由于模态使用的承诺,你应该肯定使用$ q这样的事情。

代码变成:

 function FakeModal(){ this.resultDeferred = $q.defer(); this.result = this.resultDeferred.promise; } FakeModal.prototype.open = function(options){ return this; }; FakeModal.prototype.close = function (item) { this.resultDeferred.resolve(item); $rootScope.$apply(); // Propagate promise resolution to 'then' functions using $apply(). }; FakeModal.prototype.dismiss = function (item) { this.resultDeferred.reject(item); $rootScope.$apply(); // Propagate promise resolution to 'then' functions using $apply(). }; // .... // Initialize the controller and a mock scope beforeEach(inject(function ($controller, $rootScope) { scope = $rootScope.$new(); fakeModal = new FakeModal(); MainCtrl = $controller('MainCtrl', { $scope: scope, $modal: fakeModal }); })); // .... it("should cancel the dialog when dismiss is called, and $scope.canceled should be true", function () { expect( scope.canceled ).toBeUndefined(); fakeModal.dismiss( "cancel" ); //Call dismiss (simulating clicking the cancel button on the modal) expect( scope.canceled ).toBe( true ); }); 

布兰特的回答显然非常棒,但是这个改变让我更加满意:

  fakeModal = opened: then: (openedCallback) -> openedCallback() result: finally: (callback) -> finallyCallback = callback 

那么在testing区域:

  finallyCallback() expect (thing finally callback does) .toEqual (what you would expect)