Injetando uma simulação em um serviço AngularJS

114

Tenho um serviço AngularJS escrito e gostaria de fazer um teste de unidade.

angular.module('myServiceProvider', ['fooServiceProvider', 'barServiceProvider']).
    factory('myService', function ($http, fooService, barService) {

    this.something = function() {
        // Do something with the injected services
    };

    return this;
});

Meu arquivo app.js tem estes registrados:

angular
.module('myApp', ['fooServiceProvider','barServiceProvider','myServiceProvider']
)

Posso testar se o DI está funcionando da seguinte forma:

describe("Using the DI framework", function() {
    beforeEach(module('fooServiceProvider'));
    beforeEach(module('barServiceProvider'));
    beforeEach(module('myServiceProvder'));

    var service;

    beforeEach(inject(function(fooService, barService, myService) {
        service=myService;
    }));

    it("can be instantiated", function() {
        expect(service).not.toBeNull();
    });
});

Isso provou que o serviço pode ser criado pelo framework DI, porém agora quero fazer um teste de unidade do serviço, o que significa simular os objetos injetados.

Como faço para fazer isso?

Eu tentei colocar meus objetos fictícios no módulo, por exemplo

beforeEach(module(mockNavigationService));

e reescrevendo a definição de serviço como:

function MyService(http, fooService, barService) {
    this.somthing = function() {
        // Do something with the injected services
    };
});

angular.module('myServiceProvider', ['fooServiceProvider', 'barServiceProvider']).
    factory('myService', function ($http, fooService, barService) { return new MyService($http, fooService, barService); })

Mas o último parece impedir o serviço de ser criado pela DI como um todo.

Alguém sabe como posso simular os serviços injetados para meus testes de unidade?

obrigado

David

BanksySan
fonte
Você pode dar uma olhada nesta minha resposta para outra pergunta, espero que possa ser útil para você.
remigio

Respostas:

183

Você pode injetar simulações em seu serviço usando $provide.

Se você tiver o seguinte serviço com uma dependência que tem um método chamado getSomething:

angular.module('myModule', [])
  .factory('myService', function (myDependency) {
        return {
            useDependency: function () {
                return myDependency.getSomething();
            }
        };
  });

Você pode injetar uma versão simulada de myDependency da seguinte maneira:

describe('Service: myService', function () {

  var mockDependency;

  beforeEach(module('myModule'));

  beforeEach(function () {

      mockDependency = {
          getSomething: function () {
              return 'mockReturnValue';
          }
      };

      module(function ($provide) {
          $provide.value('myDependency', mockDependency);
      });

  });

  it('should return value from mock dependency', inject(function (myService) {
      expect(myService.useDependency()).toBe('mockReturnValue');
  }));

});

Observe que, por causa da chamada para $provide.valuevocê, na verdade, você não precisa injetar explicitamente myDependency em qualquer lugar. Acontece sob o capô durante a injeção de myService. Ao configurar o mockDependency aqui, ele poderia facilmente ser um espião.

Obrigado a loyalBrown pelo link para aquele ótimo vídeo .

John galambos
fonte
13
Funciona muito bem, mas cuidado com um detalhe: a beforeEach(module('myModule'));chamada TEM que vir antes da beforeEach(function () { MOCKING })chamada, senão as simulações serão substituídas pelos serviços reais!
Nikos Paraskevopoulos
1
Existe uma maneira de zombar não do serviço, mas constante da mesma maneira?
Artem
5
Semelhante ao comentário de Nikos, todas as $providechamadas devem ser feitas antes de usar $injector, caso contrário, você receberá um erro:Injector already created, can not register a module!
providencemac
7
E se o seu mock precisar de $ q? Então você não pode injetar $ q no mock antes de chamar module () para registrar o mock. Alguma ideia?
Jake
4
Se você estiver usando coffeescript e estiver vendo Error: [ng:areq] Argument 'fn' is not a function, got Object, certifique-se de colocar um returnna linha depois $provide.value(...). Voltar implicitamente $provide.value(...)causou esse erro para mim.
yndolok
4

A meu ver, não há necessidade de zombar dos próprios serviços. Simplesmente simule as funções do serviço. Dessa forma, você pode fazer com que o angular injete seus serviços reais como faz em todo o aplicativo. Em seguida, simule as funções no serviço conforme necessário usando a spyOnfunção do Jasmine .

Agora, se o serviço em si é uma função, e não um objeto com o qual você pode usar spyOn, há outra maneira de fazer isso. Eu precisava fazer isso e encontrei algo que funciona muito bem para mim. Consulte Como você simula o serviço Angular que é uma função?

dnc253
fonte
3
Não acho que isso responda à pergunta. E se a fábrica do serviço que está sendo simulado fizer algo não trivial, como acessar o servidor para obter dados? Esse seria um bom motivo para zombar disso. Você deseja evitar a chamada do servidor e, em vez disso, criar uma versão simulada do serviço com dados falsos. Zombar de $ http também não é uma boa solução, porque, na verdade, você está testando dois serviços em um teste, em vez de testar a unidade dos dois serviços isoladamente. Portanto, gostaria de reiterar a pergunta. Como você passa um serviço simulado para outro serviço em um teste de unidade?
Patrick Arnesen
1
Se você está preocupado com o serviço atingindo o servidor para obter dados, é para isso que serve $ httpBackend ( docs.angularjs.org/api/ngMock.$httpBackend ). Não tenho certeza do que mais seria uma preocupação na fábrica do serviço que exigiria zombar de todo o serviço.
dnc253
2

Outra opção para ajudar a tornar a simulação de dependências mais fácil no Angular e Jasmine é usar o QuickMock. Ele pode ser encontrado no GitHub e permite que você crie simulações simples de uma forma reutilizável. Você pode cloná-lo do GitHub por meio do link abaixo. O README é bastante autoexplicativo, mas espero que possa ajudar outras pessoas no futuro.

https://github.com/tennisgent/QuickMock

describe('NotificationService', function () {
    var notificationService;

    beforeEach(function(){
        notificationService = QuickMock({
            providerName: 'NotificationService', // the provider we wish to test
            moduleName: 'QuickMockDemo',         // the module that contains our provider
            mockModules: ['QuickMockDemoMocks']  // module(s) that contains mocks for our provider's dependencies
        });
    });
    ....

Ele gerencia automaticamente todo o boilerplate mencionado acima, então você não precisa escrever todo o código de injeção de simulação em cada teste. Espero que ajude.

tenista
fonte
2

Além da resposta de John Galambos : se você apenas quer zombar de métodos específicos de um serviço, pode fazer assim:

describe('Service: myService', function () {

  var mockDependency;

  beforeEach(module('myModule'));

  beforeEach(module(function ($provide, myDependencyProvider) {
      // Get an instance of the real service, then modify specific functions
      mockDependency = myDependencyProvider.$get();
      mockDependency.getSomething = function() { return 'mockReturnValue'; };
      $provide.value('myDependency', mockDependency);
  });

  it('should return value from mock dependency', inject(function (myService) {
      expect(myService.useDependency()).toBe('mockReturnValue');
  }));

});
Ignitor
fonte
1

Se o seu controlador foi escrito para assumir uma dependência como esta:

app.controller("SomeController", ["$scope", "someDependency", function ($scope, someDependency) {
    someDependency.someFunction();
}]);

então você pode fazer um falso someDependencyem um teste Jasmine como este:

describe("Some Controller", function () {

    beforeEach(module("app"));


    it("should call someMethod on someDependency", inject(function ($rootScope, $controller) {
        // make a fake SomeDependency object
        var someDependency = {
            someFunction: function () { }
        };

        spyOn(someDependency, "someFunction");

        // this instantiates SomeController, using the passed in object to resolve dependencies
        controller("SomeController", { $scope: scope, someDependency: someDependency });

        expect(someDependency.someFunction).toHaveBeenCalled();
    }));
});
CodingWithSpike
fonte
9
A questão é sobre serviços, que não são instanciados no conjunto de testes com uma chamada para qualquer serviço equivalente como $ controller. Em outras palavras, não se chama $ service () em um bloco beforeEach, passando dependências.
Morris Singer
1

Recentemente, lancei o ngImprovedTesting que deve tornar o teste de simulação em AngularJS mais fácil.

Para testar 'myService' (do módulo "myApp") com suas dependências fooService e barService simuladas, você pode fazer o seguinte em seu teste Jasmine:

beforeEach(ModuleBuilder
    .forModule('myApp')
    .serviceWithMocksFor('myService', 'fooService', 'barService')
    .build());

Para obter mais informações sobre o ngImprovedTesting, confira sua postagem introdutória no blog: http://blog.jdriven.com/2014/07/ng-improved-testing-mock-testing-for-angularjs-made-easy/

Emil van Galen
fonte
1
Por que isso foi rejeitado? Eu não entendo o valor de uma votação negativa sem um comentário.
Jacob Brewer
0

Eu sei que essa é uma questão antiga, mas há outra maneira mais fácil, você pode criar mock e desabilitar o original injetado em uma função, isso pode ser feito usando spyOn em todos os métodos. veja o código abaixo.

var mockInjectedProvider;

    beforeEach(function () {
        module('myModule');
    });

    beforeEach(inject(function (_injected_) { 
      mockInjectedProvider  = mock(_injected_);
    });

    beforeEach(inject(function (_base_) {
        baseProvider = _base_;
    }));

    it("injectedProvider should be mocked", function () {
    mockInjectedProvider.myFunc.andReturn('testvalue');    
    var resultFromMockedProvider = baseProvider.executeMyFuncFromInjected();
        expect(resultFromMockedProvider).toEqual('testvalue');
    }); 

    //mock all service methods
    function mock(angularServiceToMock) {

     for (var i = 0; i < Object.getOwnPropertyNames(angularServiceToMock).length; i++) {
      spyOn(angularServiceToMock,Object.getOwnPropertyNames(angularServiceToMock)[i]);
     }
                return angularServiceToMock;
    }
Gal Morad
fonte