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

时间:2015-07-17 18:44:30

标签: angularjs unit-testing mocha sinon 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;}
            });
        };

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

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

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

我正在使用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();
});

实际测试:

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时,有没有人可以分享正确的方法?

1 个答案:

答案 0 :(得分:13)

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

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

1。确定目标

  

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

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

$modal.open&#39; 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()? 
});

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

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

假设您在控制器实现中有以下内容:

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

  someDependency.doSomething();
});

如果你想为throw编写一个测试,但是你放弃了createController方法 - 那么你需要设置一个单独的describe块。自己beforeEach|before调用设置someDependency = undefined重大麻烦!

使用&#34;延迟$ inject&#34; ,它很简单:

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

  function fn () {
    createController();
  }

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

[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!

但是,一般情况下,我会在个人it()中推荐明确的回复陈述。

3。编写规范

好的,所以回到手边的问题。

鉴于前面提到的beforeEach阻止,您的describe/it看起来像这样:

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

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

  • 用户对象。
  • 致电$scope.showProfile
  • 调用 $ modal的解析函数返回值的期望。开

问题在于测试一些不在我们手中的东西。幕后的$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);
});
  

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

     

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

我会说我们的规范涵盖了这一点 - 而且我们已经取消了&#39; $ modal启动。 甜。

custom matcherssinonjs docs的解释。

  

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

本质上;

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

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

$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上有多个方法(或应该被删除)。因此,我认为在$modal.open下屏蔽modalSpy是一个坏主意 - 它对yield的方法不是很明确。

其次,当您将存根引用为spy时,您正在混合stubmodalSpy(我会一直这样做......)。

spy包装原始功能并保留原始功能,记录所有事件&#39;对于即将到来的期望,这真的是关于它的。

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

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

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

简短而甜蜜(关于产量/产量,我知道的很多);

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

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

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