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

Don*_*rty 7 unit-testing mocha.js sinon angularjs karma-mocha

我正在尝试在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;}
            });
        };
Run Code Online (Sandbox Code Playgroud)

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

 <button class='btn btn-info' showProfile(user)'>See Profile</button>
Run Code Online (Sandbox Code Playgroud)

正如您所看到的那样,用户被传入并在模态中使用,然后数据被绑定到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)
    },
    controller:function($scope,$modalInstance,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();
});
Run Code Online (Sandbox Code Playgroud)

实际测试:

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
        });
    });
Run Code Online (Sandbox Code Playgroud)

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

使用Mocha + SinonJS代替Jasmine时,有没有人可以分享正确的方法?

Kas*_*wau 14

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

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

1.确定目标

我想验证传入的用户是否与模态的resolve参数中使用的用户相同.

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

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

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

说完看着你的测试套件,你似乎有一个把手已经(甜!),所以我们将不会对背后的推理触摸太多太多.

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

应该调用$ modal.open,并且其resolve.user函数应该返回传递给$ 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()? 
});
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

  someDependency.doSomething();
});
Run Code Online (Sandbox Code Playgroud)

如果你想为它编写一个测试throw,但是你已经通过了这个createController方法 - 那么你需要设置一个单独的describe块,它有自己的beforeEach|beforeset调用someDependency = undefined.重大麻烦!

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

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

  function fn () {
    createController();
  }

  expect(fn).to.throw(/dependency missing/i);
});
Run Code Online (Sandbox Code Playgroud)

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


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

从本质上讲,这使得单元测试角度代码变得轻而易举,因为您将永远不会Error: $injector:unpr Unknown Provider再次遇到单元测试,只需简单地杀死对灵活,集中的单元测试的无趣代码的任何和所有引用.


[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!
Run Code Online (Sandbox Code Playgroud)

但是,一般来说,我会建议个人it()的明确的回报陈述.

3.编写规范

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

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

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

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

  • 用户对象.
  • 打电话给$scope.showProfile.
  • 调用的 $ modal.openresolve函数返回值的期望.

问题在于测试一些不在我们手中的东西.什么$modal.open()做幕后不规范套件控制器的范围-这是一个依赖和依赖关系得到掐灭.

然而,我们可以测试我们的控制器调用$modal.open与正确的参数,可以,但之间的关系resolvecontroller不是本规范套件的拍拍(后面有更多介绍).

所以要修改我们的需求:

  • 用户对象.
  • 打电话给$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);
});
Run Code Online (Sandbox Code Playgroud)

我想验证传入的用户是否与模态的resolve参数中使用的用户相同.

"$ modal.open应该已经实例化,其resolve.user函数应该返回传递给$ scope.showProfile的用户."

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

对来自sinonjs文档自定义匹配器的解释.

自定义匹配器是在sinon.match工厂中创建的,它具有测试功能和可选消息.测试函数将值作为唯一参数,true如果值与期望匹配则返回,false否则返回.消息字符串用于在值与期望值不匹配时生成错误消息.

在本质上;

sinon.match(function (value) {
  return /* expectation on the behaviour/nature of value */
}, 'optional_message');
Run Code Online (Sandbox Code Playgroud)

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

$modal.open({
  // controller: function () {},
  controller: 'NamedModalController'
});
Run Code Online (Sandbox Code Playgroud)

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

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

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

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)
  },
};
Run Code Online (Sandbox Code Playgroud)

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

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;
});
Run Code Online (Sandbox Code Playgroud)

首先,你有多种方法$modal(或应该是).因此,我认为掩盖$modal.open下是一个坏主意modalSpy- 它对于哪种方法不是很明确yield.

其次,你混合spy使用stub(我做这一切的时候......)引用您的存根时,modalSpy.

A spy包装原始功能并留下它,记录即将到来的期望的所有"事件",这就是真的.

stub是一个有效spy,与我们可以改变通过提供所述功能的行为的差异.returns(),.throws()等等.在短; 一个狡猾的间谍.

与错误消息建议一样,该函数yield在调用之后才能使用.

  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...
  });
Run Code Online (Sandbox Code Playgroud)

如果我们要stub.yield('throwing errors!');从此规范中删除该行,则输出将如下所示:

LOG: Object{0: 'y'}
LOG: Object{0: 'x'}
Run Code Online (Sandbox Code Playgroud)

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

  • yield 在你的存根/间谍回调的调用之后.
  • yields 在你的存根/间谍回调的调用之前.

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


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

  • 很好的答案.我之前没有见过/使用过sinon.match,你很好地解释了它. (2认同)
  • 很棒的帖子,非常感谢详细解释 (2认同)