Unit testing AngularJS单元测试中的模拟$modal

Unit testing AngularJS单元测试中的模拟$modal,unit-testing,angularjs,angular-ui-bootstrap,Unit Testing,Angularjs,Angular Ui Bootstrap,我正在为一个控制器编写一个单元测试,它启动一个$modal,并使用返回的承诺执行一些逻辑。我可以测试触发$modal的父控制器,但我一辈子都搞不清楚如何模拟成功的承诺 我尝试了很多方法,包括使用$q和$scope.$apply()强制解决承诺。然而,我得到的最接近的答案是把一些类似于SO post中最后一个答案的东西放在一起 我在“old”$dialog模式中见过几次这样的问题。 我找不到太多关于如何使用“新建”$dialog模式的信息 一些指点会很感激的 为了说明这个问题,我使用了,并做了一些

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

我尝试了很多方法,包括使用
$q
$scope.$apply()
强制解决承诺。然而,我得到的最接近的答案是把一些类似于SO post中最后一个答案的东西放在一起

我在“old”
$dialog
模式中见过几次这样的问题。 我找不到太多关于如何使用“新建”
$dialog
模式的信息

一些指点会很感激的

为了说明这个问题,我使用了,并做了一些小的修改

控制器(主控制器和模式控制器)

视图(main.html)


在beforeach中监视$modal.open函数时

spyOn($modal, 'open').andReturn(fakeModal);

or 

spyOn($modal, 'open').and.returnValue(fakeModal); //For Jasmine 2.0+
您需要返回$modal.open通常返回的模拟,而不是$modal的模拟,它不包括您在
fakeModal
mock中设置的
open
函数。伪模式必须有一个
结果
对象,该对象包含一个
然后
函数来存储回调(在单击确定或取消按钮时调用)。它还需要一个
关闭
功能(模拟在模式上单击OK按钮)和
解除
功能(模拟在模式上单击Cancel按钮)。
关闭
关闭
函数在调用时调用必要的回调函数

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

$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());
});
一旦设置了cancel标志,单元测试将如下所示:

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

布兰特的回答显然很棒,但这一改变让我感觉更好:

  fakeModal =
    opened:
      then: (openedCallback) ->
        openedCallback()
    result:
      finally: (callback) ->
        finallyCallback = callback
然后在测试区域:

  finallyCallback()

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

为了补充Brant的答案,这里有一个稍微改进的mock,可以让您处理其他一些场景

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()
处理程序样式一起使用,而不是将两个函数(
successCallback,errorCallback
)传递给
。然后()
,例如:

modalInstance
    .result
    .then(function () {
        // close hander
    })
    .catch(function () {
        // dismiss handler
    })
    .finally(function () {
        // finally handler
    });
既然使用承诺,你肯定应该使用这些东西

代码变为:

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

明亮的非常感谢。我完全错过了open函数实际返回的内容,我试图模拟$modal本身。这很有道理。我已经为此奋斗了很久,现在可以看到前进的道路。谢谢。不客气,我很高兴它对你有用。希望UI引导将提供一个默认的$modal mock,我们可以在将来使用它在模式的结果中,我有一个服务调用
SessionService.set('lang',selectedItem)。是否可以测试服务是否在
scope.modalInstance.close('FR')之后被调用?@lightalex您可以在服务的“set”功能上使用Jasmine spy,然后期望它已被调用。与此类似:
spyOn(SessionService,'set')。和callthrough();范围.modalInstance.close('FR');expect(SessionService.set).toHaveBeenCalled()在这方面遇到一些问题。我的scope.modalInstance未定义..你们得到的是有效的modalInstance吗?嗨,如果我想测试modalInstance控制器(在本例中是ModalInstanceCtrl),那么最好的方法是什么?Itsak:我把你们的评论变成了一个完整的问题。我也被困在这上面了。问题是:我的5美分和茉莉花>=2你应该使用间谍($modal,'open')。和.callFake(fakeModal);这对我来说很有用,是模拟Promise的最好方法。FakeModal中没有定义$rootScope,那么如何在close和dimiss函数中访问它呢?抱歉,我是Angular和Jasmine的新手,我觉得这与作用域继承有关,但我看不出FakeModal是如何获得它的。我认为您的arrow函数中缺少了-可以说,这会使
->
变成
=>
,对吗?我想您要添加的是,他可以将
close:function(item){this.result.confirmCallBack(item);},
替换为
close:this.result.confirmCallBack,
,对吗?我不确定第一个代码片段中编写的代码在做什么。(☉_☉)
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();
    }
};
modalInstance
    .result
    .then(function () {
        // close hander
    })
    .catch(function () {
        // dismiss handler
    })
    .finally(function () {
        // finally handler
    });
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 );
});