我该如何模拟在AngularJS Jasmine单元测试中返回诺言的服务?


152

我有myService那个用途myOtherService,它可以进行远程调用,并返回promise:

angular.module('app.myService', ['app.myOtherService'])
  .factory('myService', [
    myOtherService,
    function(myOtherService) {
      function makeRemoteCall() {
        return myOtherService.makeRemoteCallReturningPromise();
      }

      return {
        makeRemoteCall: makeRemoteCall
      };      
    }
  ])

要对myService我进行单元测试,需要模拟myOtherService,以使其makeRemoteCallReturningPromise方法返回promise。这是我的方法:

describe('Testing remote call returning promise', function() {
  var myService;
  var myOtherServiceMock = {};

  beforeEach(module('app.myService'));

  // I have to inject mock when calling module(),
  // and module() should come before any inject()
  beforeEach(module(function ($provide) {
    $provide.value('myOtherService', myOtherServiceMock);
  }));

  // However, in order to properly construct my mock
  // I need $q, which can give me a promise
  beforeEach(inject(function(_myService_, $q){
    myService = _myService_;
    myOtherServiceMock = {
      makeRemoteCallReturningPromise: function() {
        var deferred = $q.defer();

        deferred.resolve('Remote call result');

        return deferred.promise;
      }    
    };
  }

  // Here the value of myOtherServiceMock is not
  // updated, and it is still {}
  it('can do remote call', inject(function() {
    myService.makeRemoteCall() // Error: makeRemoteCall() is not defined on {}
      .then(function() {
        console.log('Success');
      });    
  }));  

从上面可以看到,我的模拟的定义取决于$q,我必须使用来加载inject()。此外,应该在中进行注入模拟module(),这应该在之前进行inject()。但是,更改模拟后,其值不会更新。

正确的方法是什么?


错误真的存在myService.makeRemoteCall()吗?如果是这样,问题就在于myService没有,makeRemoteCall与您的嘲笑无关myOtherService
dnc253

该错误发生在myService.makeRemoteCall()上,因为myService.myOtherService此时仅是一个空对象(其值从未按角度进行更新)
Georgii Oleinikov 2014年

将空对象添加到ioc容器中,然后更改引用myOtherServiceMock以指向要监视的新对象。随着引用的更改,ioc容器中的内容不会反映出来。
twDuke

Answers:


175

我不确定为什么您的方法行不通,但是我通常使用该spyOn函数来完成。像这样:

describe('Testing remote call returning promise', function() {
  var myService;

  beforeEach(module('app.myService'));

  beforeEach(inject( function(_myService_, myOtherService, $q){
    myService = _myService_;
    spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() {
        var deferred = $q.defer();
        deferred.resolve('Remote call result');
        return deferred.promise;
    });
  }

  it('can do remote call', inject(function() {
    myService.makeRemoteCall()
      .then(function() {
        console.log('Success');
      });    
  }));

还要记住,您将需要$digest调用要调用的then函数。请参阅$ q文档的“ 测试”部分。

- - - 编辑 - - -

在仔细研究您的操作之后,我认为我在您的代码中看到了问题。在中beforeEach,您将设置myOtherServiceMock为一个新对象。将$provide永远不会看到这个参考。您只需要更新现有参考:

beforeEach(inject( function(_myService_, $q){
    myService = _myService_;
    myOtherServiceMock.makeRemoteCallReturningPromise = function() {
        var deferred = $q.defer();
        deferred.resolve('Remote call result');
        return deferred.promise;   
    };
  }

1
昨天你没露面就杀了我。美丽的andCallFake()显示。谢谢。
Priya Ranjan Singh 2014年

代替andCallFake您可以使用andReturnValue(deferred.promise)(或and.returnValue(deferred.promise)在Jasmine 2.0+中)。当然,您需要deferred在调用之前进行定义spyOn
乔丹

1
$digest如果您无法访问示波器,在这种情况下您将如何称呼?
Jim Aho 2015年

7
@JimAho通常,您只需注入$rootScope并调用$digest它。
dnc253

1
在这种情况下,无需使用延迟。您可以只使用$q.when() codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong
fodma1 '16

69

我们还可以直接通过间谍编写茉莉的实现诺言的实现。

spyOn(myOtherService, "makeRemoteCallReturningPromise").andReturn($q.when({}));

对于茉莉花2:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));

(摘自评论,感谢ccnokes)


12
注意使用Jasmine 2.0的人,.andReturn()已由.and.returnValue代替。因此,上面的示例是:spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));我花了半个小时才弄清楚这一点。
ccnokes 2015年

13
describe('testing a method() on a service', function () {    

    var mock, service

    function init(){
         return angular.mock.inject(function ($injector,, _serviceUnderTest_) {
                mock = $injector.get('service_that_is_being_mocked');;                    
                service = __serviceUnderTest_;
            });
    }

    beforeEach(module('yourApp'));
    beforeEach(init());

    it('that has a then', function () {
       //arrange                   
        var spy= spyOn(mock, 'actionBeingCalled').and.callFake(function () {
            return {
                then: function (callback) {
                    return callback({'foo' : "bar"});
                }
            };
        });

        //act                
        var result = service.actionUnderTest(); // does cleverness

        //assert 
        expect(spy).toHaveBeenCalled();  
    });
});

1
这就是我过去所做的。创建一个间谍,该间谍返回模仿“当时”的假货
Darren Corbett 2015年

您能否提供一个完整测试的示例。我有一个类似的问题,即有一个服务会返回一个Promise,但是在其中也会进行一个返回Promise的呼叫!
Rob Paddock 2015年

嗨,罗布,您不确定为什么要模拟一个模拟对另一个服务的调用,您肯定想在测试该函数时对其进行测试。如果您要嘲笑的函数调用是服务获取数据,然后影响您的嘲笑的诺言将返回伪造的受影响数据集的数据,至少我会这样做。
Darren Corbett 2015年

我从这条道路开始,它非常适合简单的场景。我什至创建了一个模拟链接的模拟程序,并提供“保持” /“中断”助手来调用链gist.github.com/marknadig/c3e8f2d3fff9d22da42b 在更复杂的情况下,这种情况会下降。就我而言,我有一个服务,可以有条件地从缓存中返回项目(带/延迟)或发出请求。因此,这是在创造自己的诺言。
Mark Nadig

这篇文章ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy完整描述了伪造的“ then”的用法。
Custodio

8

您可以使用诸如sinon之类的存根库来模拟您的服务。然后,您可以返回$ q.when()作为您的承诺。如果作用域对象的值来自promise结果,则需要调用scope。$ root。$ digest()。

var scope, controller, datacontextMock, customer;
  beforeEach(function () {
        module('app');
        inject(function ($rootScope, $controller,common, datacontext) {
            scope = $rootScope.$new();
            var $q = common.$q;
            datacontextMock = sinon.stub(datacontext);
            customer = {id:1};
           datacontextMock.customer.returns($q.when(customer));

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

        })
    });


    it('customer id to be 1.', function () {


            scope.$root.$digest();
            expect(controller.customer.id).toBe(1);


    });

2
这是缺少的部分,要求$rootScope.$digest()得到解决的诺言

2

使用sinon

const mockAction = sinon.stub(MyService.prototype,'actionBeingCalled')
                     .returns(httpPromise(200));

已知httpPromise可以是:

const httpPromise = (code) => new Promise((resolve, reject) =>
  (code >= 200 && code <= 299) ? resolve({ code }) : reject({ code, error:true })
);

0

老实说..您通过依赖注入来模拟服务而不是模块来解决这种错误的方法。同样,在beforeEach中调用inject是一种反模式,因为它会使每个测试的模拟变得困难。

这是我要怎么做...

module(function ($provide) {
  // By using a decorator we can access $q and stub our method with a promise.
  $provide.decorator('myOtherService', function ($delegate, $q) {

    $delegate.makeRemoteCallReturningPromise = function () {
      var dfd = $q.defer();
      dfd.resolve('some value');
      return dfd.promise;
    };
  });
});

现在,当您注入服务时,它将具有正确模拟的使用方法。


3
每次之前的全部要点是在每次测试之前调用它,我不知道您如何编写测试,但是就我个人而言,我为一个函数编写了多个测试,因此我将有一个通用的基础设置,该调用之前每次测试。同样,您可能希望查找反模式与软件工程相关的理解含义。
Darren Corbett

0

我发现有用的,刺入的服务功能为sinon.stub()。returns($ q.when({})):

this.myService = {
   myFunction: sinon.stub().returns( $q.when( {} ) )
};

this.scope = $rootScope.$new();
this.angularStubs = {
    myService: this.myService,
    $scope: this.scope
};
this.ctrl = $controller( require( 'app/bla/bla.controller' ), this.angularStubs );

控制器:

this.someMethod = function(someObj) {
   myService.myFunction( someObj ).then( function() {
        someObj.loaded = 'bla-bla';
   }, function() {
        // failure
   } );   
};

并测试

const obj = {
    field: 'value'
};
this.ctrl.someMethod( obj );

this.scope.$digest();

expect( this.myService.myFunction ).toHaveBeenCalled();
expect( obj.loaded ).toEqual( 'bla-bla' );

-1

代码段:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() {
    var deferred = $q.defer();
    deferred.resolve('Remote call result');
    return deferred.promise;
});

可以用更简洁的形式编写:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue(function() {
    return $q.resolve('Remote call result');
});
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.