在 AngularJS 单元测试中模拟 $modal

Posted

技术标签:

【中文标题】在 AngularJS 单元测试中模拟 $modal【英文标题】:Mocking $modal in AngularJS unit tests 【发布时间】:2014-02-08 11:50:35 【问题描述】:

我正在为一个控制器编写一个单元测试,该控制器启动 $modal 并使用返回的承诺来执行一些逻辑。我可以测试触发 $modal 的父控制器,但我一生都无法弄清楚如何模拟一个成功的承诺。

我尝试了很多方法,包括使用$q$scope.$apply() 来强制解决promise。但是,我得到的最接近的是将类似于 this SO 帖子中最后一个答案的内容放在一起;

我见过几次用“旧”$dialog 模态询问这个问题。 我找不到太多关于如何使用“新”$dialog 模态来做到这一点。

一些指针将不胜感激。

为了说明问题,我使用了example provided in the UI Bootstrap docs,并进行了一些小的修改。

控制器(主要和模式)

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

【问题讨论】:

嗨,如果我想测试 modalInstance 控制器(在本例中为 ModalInstanceCtrl),最好的方法是什么? Itsak:我把你的评论变成了一个完整的问题。我也坚持这一点。问题在这里:***.com/q/24373220/736963 我的 5 cents with jasmine >= 2 你应该使用 spyOn($modal, 'open').and.callFake(fakeModal); 【参考方案1】:

当你在 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 函数来存储回调(在单击 OK 或 Cancel 按钮时调用)。它还需要一个close 函数(模拟一个确定按钮单击模态)和一个dismiss 函数(模拟一个取消按钮单击模态)。 closedismiss 函数在调用时会调用必要的回调函数。

fakeModal更改为以下,单元测试将通过:

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

此外,您可以通过在取消处理程序中添加要测试的属性来测试取消对话框案例,在本例中为$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());
);

一旦设置了取消标志,单元测试将如下所示:

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

【讨论】:

太棒了!非常感谢。我完全错过了 open 函数实际返回的内容,并且我试图模拟 $modal 本身。这很有意义。我已经为此奋斗了很久,现在可以看到前进的方向。欣赏它。 不客气,我很高兴它对你有用。希望 UI Bootstrap 将提供一个默认的 $modal 模拟,我们可以在未来使用。 在模态结果中没有$scope.selected = selectedItem;,我有一个服务调用SessionService.set('lang', selectedItem);。是否可以测试是否在scope.modalInstance.close('FR'); 之后立即调用了该服务? @lightalex 您可以在服务的“设置”功能上使用 Jasmine 间谍,然后期望它已被调用。类似这样:spyOn( SessionService, 'set' ).andCallThrough(); scope.modalInstance.close('FR'); expect( SessionService.set ).toHaveBeenCalled(); 在这方面遇到了一些麻烦。我的 scope.modalInstance 是未定义的..你们得到了一个有效的 modalInstance 吗?【参考方案2】:

为了补充 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();
    
;

这将允许模拟处理...

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

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

【讨论】:

【参考方案3】:

由于modals 使用承诺,您绝对应该将$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 );
);

【讨论】:

这对我有用,是模拟 Promise 的最佳方式 我没有看到在 FakeModal 中定义了 $rootScope,那么如何在 close 和 dimiss 函数中访问它?抱歉,我是 Angular 和 Jasmine 的新手,我觉得它与范围继承有关,但我看不出 FakeModal 是如何得到它的。【参考方案4】:

Brant 的回答显然很棒,但这种变化让我觉得更好:

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

然后在测试区:

  finallyCallback()

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

【讨论】:

我认为您在箭头函数中缺少 - 可以说,这会使 -&gt; 变为 =&gt;,对吗?而且我认为您要补充的是,他可以用close: this.result.confirmCallBack, 替换close: function( item ) this.result.confirmCallBack( item ); ,,对吗?我不确定在第一个 sn-p 中编写的代码在做什么。 (☉_☉)

以上是关于在 AngularJS 单元测试中模拟 $modal的主要内容,如果未能解决你的问题,请参考以下文章

在 Jasmine 单元测试中模拟 AngularJS 模块依赖项

如何在 AngularJS 单元测试中模拟 $window.location.replace?

如何模拟在 AngularJS Jasmine 单元测试中返回承诺的服务?

在AngularJS单元测试中模拟模块依赖性

基于karma和jasmine的Angularjs 单元测试

Angularjs 基于karma和jasmine的单元测试