7

我正在尝试在 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 来尝试执行单元测试,但我不明白如何实现这一点,我想验证传入的用户是否与模式的解析参数中使用的用户相同。

我已经看到了一些如何使用 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 时采取的正确方法吗?

4

1 回答 1

16

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

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

1.确立目标

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

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

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

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

看过您的测试套件后,您似乎已经掌握了这一点(太好了!)所以我们不必过多地讨论背后的推理。

我想期望的最初措辞会是这样的:

$modal.open 应该已经被调用,并且它的 resolve.user 函数应该返回传递给 $scope.showProfile 的用户。

2. 准备

我现在要从你的测试套件中删掉很多东西,以使其更具可读性。如果缺少对规范通过至关重要的部分,我深表歉意。

之前每个

我将从简化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这是我们在为 Angular 控制器编写单元测试时已经在我的公司建立了很长一段时间的东西。它为您在每个规范的基础上修改所述控制器依赖项提供了很大的灵活性。

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

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

  someDependency.doSomething();
});

如果您想为. throw_ 大麻烦!createControllerdescribebeforeEach|beforesomeDependency = undefined

使用"delayed $inject",它很简单:

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

  function fn () {
    createController();
  }

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

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


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

从本质上讲,这使得单元测试 Angular 代码变得轻而易举,因为您将永远不会再遇到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.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);
});

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

“$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');

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

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

这样,您可以为模态控制器(当然在另一个规范文件中)编写期望,如下所示:

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.

其次,spystub您将存根引用为modalSpy.

Aspy包装原始功能并保留它,记录所有“事件”以供即将到来的期望,仅此而已。

Astub实际上是 a ,不同之处在于我们可以通过提供等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在调用存根/间谍回调之前。

如果你已经走到了这一步,你可能已经意识到我可以连续几个小时就这个主题喋喋不休。幸运的是,我累了,是时候闭上眼睛了。


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

于 2015-07-17T22:41:59.130 回答