微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


ANGULAR.JS:NG-SELECTANDNG-OPTIONSPS:其实看英文文档比看中文文档更容易理解,前提是你的英语基础还可以。英文文档对于知识点讲述简明扼要,通俗易懂,而有些中文文档读起来特别费力,基础差、底子薄的有可能一会就会被绕晕了,最起码英文文档中的代码与中文文档中的代码是一致的,但知识点讲述实在是差距太大。Angular.jshasapowerfuldire
AngularJS中使用Chart.js制折线图与饼图实例  Chart.js 是一个令人印象深刻的 JavaScript 图表库,建立在 HTML5 Canvas 基础上。目前,它支持6种图表类型(折线图,条形图,雷达图,饼图,柱状图和极地区域区)。而且,这是一个独立的包,不依赖第三方 JavaScript 库,小于 5KB。   其中用到的软件:   Chart.js框架,版本1.0.2,一
IE浏览器兼容性后续前言 继续尝试解决IE浏览器兼容性问题,结局方案为更换jquery、angularjs、IE的版本。 1.首先尝试更换jquery版本为1.7.2 jquery-1.9.1.js-->jquery-1.7.2.js--> jquery2.1.4.js 无效 2.尝试更换IE版本IE8 IE11-
Angular实现下拉菜单多选写这篇文章时,引用文章地址如下:http://ngmodules.org/modules/angularjs-dropdown-multiselecthttp://dotansimha.github.io/angularjs-dropdown-multiselect/#/AngularJSDropdownMultiselectThisdire
在AngularJS应用中集成科大讯飞语音输入功能前言 根据项目需求,需要在首页搜索框中添加语音输入功能,考虑到科大讯飞语音业务的强大能力,遂决定使用科大讯飞语音输入第三方服务。软件首页截图如下所示: 涉及的源代码如下所示: //语音识别$rootScope.startRecognize = function() {var speech;
Angular数据更新不及时问题探讨前言 在修复控制角标正确变化过程中,发觉前端代码组织层次出现了严重问题。传递和共享数据时自己使用的是rootScope,为此造成了全局变量空间的污染。根据《AngularJs深度剖析与最佳实践》,如果两个控制器的协作存在大量的数据共享和交互可以利用Factory等服务的“单例”特性为它们注入一个共享对象来传递数据。而自己在使用rootScope
HTML:让表单、文本框只读,不可编辑的方法有时候,我们希望表单中的文本框是只读的,让用户不能修改其中的信息,如使中国">的内容,"中国"两个字不可以修改。实现的方式归纳一下,有如下几种。方法1:onfocus=this.blur()中国"onfocus=this.blur()>方法2:readonly中国"readonly>中国"readonly="tru
在AngularJS应用中实现微信认证授权遇到的坑前言 项目开发过程中,移动端新近增加了一个功能“微信授权登录”,由于自己不是负责移动端开发的,但最后他人负责的部分未达到预期效果。不能准确实现微信授权登录。最后还得靠自己做进一步的优化工作,谁让自己是负责人呢?原来负责人就是负责最后把所有的BUG解决掉。 首先,熟悉一下微信授权部分的源代码,如下所示:
AngularJS实现二维码信息的集成思路需求 实现生成的二维码包含订单详情信息。思路获取的内容数据如下: 现在可以获取到第一级数据,第二级数据data获取不到。利用第一级数据的获取方法获取不到第二级数据。for(i in data){alert(i); //获得属性 if(typeof(data[i]) == "o
Cookie'data'possiblynotsetoroverflowedbecauseitwastoolarge(5287>4096bytes)!故事起源 项目开发过程中遇到以上问题,刚开始以为只是个警告,没太在意。后来发现直接影响到了程序的执行效果。果断寻找解决方法。问题分析 根据Chrome浏览器信息定位,显示以下代码存在错误: