angularjs – 如何使用Sinon.js测试Angular $modal?

我正在尝试在AngularJS中为$modal编写单元测试.模态的代码位于控制器中,如下所示:
$scope.showProfile = function(user){
                var modalinstance = $modal.open({
                templateUrl:"components/profile/profile.html",resolve:{
                    user:function(){return user;}
                },controller:function($scope,$modalinstance,user){$scope.user=user;}
            });
        };

函数在HTML中的ng-repeat中的按钮上调用,如下所示:

<button class='btn btn-info' showProfile(user)'>See Profile</button>

正如您所看到的那样,用户被传入并在模态中使用,然后数据被绑定到HTML中的profile部分.

我正在使用Karma-Mocha和Karma-Sinon来尝试执行单元测试,但我无法理解如何实现这一点,我想验证传入的用户是与模态的resolve参数中使用的相同.

我已经看到了一些如何使用Jasmine进行此操作的示例,但我无法将它们转换为mocha sinon测试.

这是我的尝试:

设置代码

describe('Unit: ProfileController Test Suite,',function(){
beforeEach(module('myApp'));

var $controller,modalSpy,modal,fakeModal;

fakeModal  = {// Create a mock object using spies
    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);
    }
};

var modalOptions = {
    templateUrl:"components/profile/profile.html",resolve:{
        agent:sinon.match.any //No idea if this is correct,trying to match jasmine.any(Function)
    },user){$scope.user=user;}
};

var actualOptions;

beforeEach(inject(function(_$controller_,_$modal_){
    // The injector unwraps the underscores (_) from around the parameter names when matching
    $controller = _$controller_;
    modal = _$modal_;
    modalSpy = sinon.stub(modal,"open");
    modalSpy.yield(function(options){ //Doesn't seem to be correct,trying to match Jasmines callFake function but get this error - open cannot yield since it was not yet invoked.
        actualOptions = options;
        return fakeModal;
    });
}));

var $scope,controller;

beforeEach(function() {
    $scope = {};

    controller = $controller('profileController',{
        $scope: $scope,$modal: modal
    });

});

afterEach(function () {
    modal.open.restore();
});

实际测试:

describe.only('display a user profile',function () {
        it('user details should match those passed in',function(){
            var user= { name : "test"};
            $scope.showProfile(user);

            expect(modalSpy.open.calledWith(modalOptions)).to.equal(true); //Always called with empty
            expect(modalSpy.open.resolve.user()).to.equal(user); //undefined error - cannot read property resolve of undefined
        });
    });

我的测试设置和实际测试基于我遇到的Jasmine代码并尝试将其转换为Mocha Sinonjs代码,我是AngularJS和编写单元测试的新手,所以我希望我只需要在正确的方向上轻推.

使用Mocha Sinonjs而不是Jasmine时,任何人都可以分享正确的方法吗?

这将是一个很长的答案,涉及单元测试,存根和sinon.js(在某种程度上).

(如果您想跳过,请向下滚动到#3标题之后,看一下您的规范的最终实现)

1.确定目标

I want to verify that the user getting passed in is the same one used in the resolve parameter of the modal.

很好,所以我们有一个目标.

$modal.open的resolve {user:fn}的返回值应该是我们传递给$scope.showProfile方法用户.

鉴于$modal是您实现中的外部依赖项,我们根本不关心$modal的内部实现.显然,我们不想将真正的$modal服务注入我们的测试套件.

看过你的测试套件后,你似乎已经抓住了它(甜蜜!)所以我们不必再触及背后的推理了.

我认为期望的初始措辞会令人痛苦:

$modal.open should have been invoked,and its resolve.user function should return the user passed to $scope.showProfile.

2.准备

我现在要从你的测试套件中删除很多东西,以便让它更具可读性.如果缺少对规范传递至关重要的部分,我道歉.

beforeEach

我将从简化beforeEach块开始.每个描述块都有一个单独的beforeEach块更简洁,它简化了可读性并减少了样板代码.

您的简化beforeEach块可能如下所示:

var $scope,$modal,createController; // [1]: createController(?)

beforeEach(function () {
  $modal = {}; // [2]: empty object? 

  module('myApp',function ($provide) {
    $provide.value('$modal',$modal); // [3]: uh? 
  });

  inject(function ($controller,$injector) { // [4]: $injector? 
    $scope = $injector.get('$rootScope').$new();
    $modal = $injector.get('$modal');

    createController = function () { // [5(1)]: createController?!
      return $controller('profileController',{
        $scope: $scope
        $modal: $modal
      });
    };
  });

  // Mock API's
  $modal.open = sinon.stub(); // [6]: sinon.stub()? 
});

所以,关于我添加/更改的内容的一些注释:

[1]:在为角度控制器编写单元测试时,createController是我们公司已经建立了很长一段时间的东西.它为您提供了很大的灵活性,可以根据规范修改所述控制器依赖性.

假设您的控制器实现中包含以下内容

.controller('...',function (someDependency) {
  if (!someDependency) {
    throw new Error('My super important dependency is missing!');  
  }

  someDependency.doSomething();
});

如果你想为throw编写一个测试,但是你在createController方法上放弃了 – 你需要在调用setDependency = undefined之前用它自己的beforeEach设置一个单独的describe块.重大麻烦!

通过“延迟$inject”,它很简单:

it('throws',function () {
  someDependency = undefined;

  function fn () {
    createController();
  }

  expect(fn).to.throw(/dependency missing/i);
});

[2]:空对象通过在beforeEach块的开头用空对象覆盖全局变量,我们可以确定前一个规范中的任何剩余方法都已死.

[3]:$provide通过$提供模拟(此时为空)对象作为模块的值,我们不必加载包含$modal实际实现的模块.

从本质上讲,这使得单元测试角度代码变得轻而易举,因为您将永远不会遇到错误:$injector:unpr未知提供程序再次在单元测试中,通过简单地杀死任何和所有对非灵敏代码的引用,专注于单元测试.

[4]:$injector我更喜欢使用$injector,因为它减少了你需要提供给inject()方法的参数数量几乎没有.请你在这里做!

[5]:createController读取#1.

[6]:sinon.stub在您的beforeEach块结束时,我建议您使用必要的方法提供所有已删除的依赖项.剔除方法.

如果你坚持认为存根方法将会并且应该总是返回,比如一个解决的承诺 – 您可以将此行更改为:

dependency.mockedFn = sinon.stub().returns($q.when());
// dont forget to expose,and $inject -> $q!

但是,一般来说,我会在个人it()中推荐明确的return语句.

3.编写规范

好的,所以回到手头的问题.

鉴于前面提到的beforeEach块,你的describe / it看起来像这样:

describe('displaying a user profile',function () {
  it('matches the passed in user details',function (){
    createController();
  });
});

有人会认为我们需要以下内容

>用户对象.
>调用$scope.showProfile.
>对调用的$modal.open的resolve函数的返回值的期望.

问题在于测试一些不在我们手中的东西. $modal.open()在幕后做的不在你控制器规范套件的范围内 – 它是一个依赖项,并且依赖项被删除了.

然而,我们可以测试我们的控制器使用正确的参数调用$modal.open,但是解析和控制器之间的关系不是这个规范套件的一部分(稍后会详细介绍).

所以要修改我们的需求:

>用户对象.
>调用$scope.showProfile.
>对传递给$modal.open的参数的期望.

it('calls $modal.open with the correct params',function (){
  // Preparation
  var user = { name: 'test' };
  var expected = {
    templateUrl: 'components/profile/profile.html',resolve: {
      user: sinon.match(function (value) {
        return value() === user;
      },'boo!')
    },controller: sinon.match.any        
  };

  // Execution
  createController();
  $scope.showProfile(user);

  // Expectation
  expect($modal.open).to.have
    .been.calledOnce
    .and.calledWithMatch(expected);
});

I want to verify that the user getting passed in is the same one used in the resolve parameter of the modal.

“$modal.open should have been instantiated,and its resolve.user function should return the user passed to $scope.showProfile.”

我会说我们的规范完全覆盖了 – 我们已经’取消’$modal来启动.甜.

sinonjs docs开始的custom matchers的解释.

Custom matchers are created with the sinon.match factory which takes a test function and an optional message. The test function takes a value as the only argument,returns true if the value matches the expectation and false otherwise. The message string is used to generate the error message in case the value does not match the expectation.

在本质上;

sinon.match(function (value) {
  return /* expectation on the behavIoUr/nature of value */
},'optional_message');

如果您绝对想要测试resolve的返回值(最终在$modal控制器中的值),我建议您通过将控制器提取到命名控制器而不是匿名函数来单独测试控制器.

$modal.open({
  // controller: function () {},controller: 'NamedModalController'
});

这样你可以写出对模态控制器的期望(当然是在另一个spec文件中):

it('exposes the resolved {user} value onto $scope',function () {
  user = { name: 'Mike' };
  createController();
  expect($scope).to.have.property('user').that.deep.equals(user);
});

现在,很多都是重复 – 你已经做了很多我所涉及的事情,这里希望我不是作为一种工具.

我提议的it()中的一些准备数据可以移动到beforeEach块 – 但我建议只有在有大量测试调用相同的代码时才这样做.

保持规范套件DRY并不像保持规范明确那样重要,以避免在另一个开发人员过来阅读它们并修复一些回归错误时出现任何混淆.

最后,您在原文中写的一些内联注释:

sinon.match.any

var modalOptions = {
  resolve:{
    agent:sinon.match.any // No idea if this is correct,trying to match jasmine.any(Function)
  },};

如果你想将它与一个函数匹配,你会这样做:

sinon.match.func,相当于jasmine.any(Function).

sinon.match.any匹配任何东西.

sinon.stub.yield([arg1,arg2])

// open cannot yield since it was not yet invoked.
modalSpy.yield(function(options){ 
  actualOptions = options;
  return fakeModal;
});

首先,你在$modal上有多个方法(或者应该是).因此,我认为在modalSpy下屏蔽$modal.open是一个坏主意 – 它不是很清楚哪种方法可以产生.

其次,当你的存根作为modalSpy引用时,你正在混合间谍和存根(我一直这样做……).

一个间谍包裹了原始的功能并留下它,记录所有即将到来的期望的’事件’,这就是真的.

存根实际上是间谍,不同之处在于我们可以通过提供.returns(),. throws()等来改变所述函数的行为.一个狡猾的间谍.

错误消息建议的一样,该函数调用之后才能生成.

it('yield / yields',function () {
    var stub = sinon.stub();

    stub.yield('throwing errors!'); // will crash...
    stub.yields('y');

    stub(function () {
      console.log(arguments);
    });

    stub.yield('x');
    stub.yields('ohno'); // wont happen...
  });

如果我们要删除stub.yield(‘抛出错误!’);从这个规范的行,输出将如下所示:

LOG: Object{0: 'y'}
LOG: Object{0: 'x'}

简短而甜蜜(就产量/产量而言,这与我所知道的差不多);

>你的存根/间谍回调调用后的收益率.
>在你的存根/间谍回调的调用之前收益.

如果你已达到这个目的,你可能已经意识到我可以连续几个小时不停地谈论这个话题.幸运的是,我已经厌倦了,现在是时候闭嘴了.

一些与该主题松散相关的资源:

> (Why) is it important that a unit test not test dependencies?
> unit test like a secret agent w/ sinon
> yield vs yields vs callsarg
> artofunittesting

相关文章

ANGULAR.JS:NG-SELECTANDNG-OPTIONSPS:其实看英文文档比看中...
AngularJS中使用Chart.js制折线图与饼图实例  Chart.js 是...
IE浏览器兼容性后续前言 继续尝试解决IE浏览器兼容性问题,...
Angular实现下拉菜单多选写这篇文章时,引用文章地址如下:h...
在AngularJS应用中集成科大讯飞语音输入功能前言 根据项目...
Angular数据更新不及时问题探讨前言 在修复控制角标正确变...