Como zombar de um serviço que retorna promessa no teste de unidade AngularJS Jasmine?

152

Eu tenho myServiceesse usos myOtherService, que faz uma chamada remota, retornando a promessa:

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

      return {
        makeRemoteCall: makeRemoteCall
      };      
    }
  ])

Para fazer um teste de unidade, myServicepreciso zombar myOtherService, de modo que seu makeRemoteCallReturningPromisemétodo retorne uma promessa. É assim que eu faço:

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

Como você pode ver acima, a definição do meu mock depende $q, da qual eu tenho que carregar usando inject(). Além disso, a injeção do mock deve estar acontecendo module(), o que deve acontecer antes inject(). No entanto, o valor da simulação não é atualizado após a alteração.

Qual é a maneira correta de fazer isso?

Georgii Oleinikov
fonte
O erro está realmente ativado myService.makeRemoteCall()? Nesse caso, o problema é myServicenão ter o makeRemoteCall, nem nada a ver com o seu escárnio myOtherService.
Dnc253
O erro está em myService.makeRemoteCall (), porque myService.myOtherService é apenas um objeto vazio neste momento (o seu valor nunca foi atualizado por angular)
Georgii Oleinikov
Você adiciona o objeto vazio ao contêiner ioc, depois altera a referência myOtherServiceMock para apontar para um novo objeto no qual você espia. O conteúdo do contêiner ioc não reflete isso, pois a referência é alterada.
TwDuke 28/08

Respostas:

175

Não sei por que o modo como você fez isso não funciona, mas geralmente faço com a spyOnfunção Algo assim:

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

Lembre-se também de que você precisará fazer uma $digestchamada para que a thenfunção seja chamada. Consulte a seção Teste da documentação $ q .

------EDITAR------

Depois de olhar mais de perto o que você está fazendo, acho que vejo o problema no seu código. No beforeEach, você está definindo myOtherServiceMockum objeto totalmente novo. O $providenunca verá esta referência. Você só precisa atualizar a referência existente:

beforeEach(inject( function(_myService_, $q){
    myService = _myService_;
    myOtherServiceMock.makeRemoteCallReturningPromise = function() {
        var deferred = $q.defer();
        deferred.resolve('Remote call result');
        return deferred.promise;   
    };
  }
dnc253
fonte
1
E você me matou ontem por não aparecer nos resultados. Bela exibição de andCallFake (). Obrigado.
Priya Ranjan Singh
Em vez de andCallFakevocê pode usar andReturnValue(deferred.promise)(ou and.returnValue(deferred.promise)no Jasmine 2.0+). Você precisa definir deferredantes de ligar spyOn, é claro.
Jordan Running
1
Como você chamaria $digestnesse caso quando não tem acesso ao escopo?
Jim Aho
7
@ JimAho Normalmente você apenas injeta $rootScopee invoca $digestisso.
Dnc253
1
O uso diferido neste caso é desnecessário. Você pode apenas usar $q.when() codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong
fodma1
69

Também podemos escrever a implementação do jasmim de retornar a promessa diretamente por espião.

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

Para Jasmine 2:

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

(copiado dos comentários, graças ao ccnokes)

Priya Ranjan Singh
fonte
12
Nota para as pessoas que usam o Jasmine 2.0, .andReturn () foi substituído por .and.returnValue. Portanto, o exemplo acima seria: spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));acabei de matar meia hora descobrindo isso.
Ccnokes
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();  
    });
});
Darren Corbett
fonte
1
Foi assim que eu fiz no passado. Criar um espião que retorna um falso que imita o "depois"
Darren Corbett
Você pode fornecer um exemplo do teste completo que você possui. Eu tenho um problema semelhante de ter um serviço que retorna uma promessa, mas com ele também faz uma ligação que retorna uma promessa!
Rob Paddock
Oi Rob, não sei por que você deseja zombar de uma chamada que um mock faz para outro serviço, com certeza você deseja testá-lo ao testar essa função. Se as chamadas de função que você está simulando chama, um serviço obtém dados e afeta os dados que sua promessa simulada retornaria um conjunto de dados afetado falso, pelo menos é assim que eu faria.
Darren Corbett
Comecei por esse caminho e funciona muito bem em cenários simples. Até criei um mock que simula o encadeamento e fornece aos auxiliares "keep" / "break" para invocar a cadeia gist.github.com/marknadig/c3e8f2d3fff9d22da42b Em cenários mais complexos, isso diminui. No meu caso, eu tinha um serviço que retornaria condicionalmente itens de um cache (com adiamento) ou faria uma solicitação. Então, estava criando sua própria promessa.
Mark Nadig
Esta publicação ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy descreve o uso de "então" falso completamente.
Custodio
8

Você pode usar uma biblioteca de stubbing como sinon para zombar de seu serviço. Você pode retornar $ q.when () como sua promessa. Se o valor do objeto do escopo vier do resultado da promessa, você precisará chamar 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);


    });
Mike Lunn
fonte
2
esta é a peça que faltava, chamando $rootScope.$digest()para obter a promessa de ser resolvido
2

usando sinon:

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

Sabido que, httpPromisepode ser:

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

Honestamente ... você está fazendo isso da maneira errada, confiando no injetar para simular um serviço em vez do módulo. Além disso, chamar injetar em um beforeEach é um anti-padrão, pois dificulta a zombaria por teste.

Aqui está como eu faria isso ...

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

Agora, quando você injeta seu serviço, ele terá um método de zombaria adequado para uso.

Keith Loy
fonte
3
O ponto principal de um antes de cada um é que ele é chamado antes de cada teste. Não sei como você escreve seus testes. Pessoalmente, escrevo vários testes para uma única função; portanto, eu teria uma base comum configurada que seria chamada antes. cada teste. Além disso, convém procurar o significado entendido de antipadrão, associado à engenharia de software.
Darren Corbett
0

Achei que a função útil e facada de serviço como 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 );

controlador:

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

e teste

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

this.scope.$digest();

expect( this.myService.myFunction ).toHaveBeenCalled();
expect( obj.loaded ).toEqual( 'bla-bla' );
Dmitri Algazin
fonte
-1

O trecho de código:

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

Pode ser escrito de uma forma mais concisa:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue(function() {
    return $q.resolve('Remote call result');
});
trunikov
fonte