Injecter une maquette dans un service AngularJS

114

J'ai un service AngularJS écrit et je voudrais le tester unitaire.

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

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

    return this;
});

Mon fichier app.js a ces derniers enregistrés:

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

Je peux tester le DI fonctionne comme tel:

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

Cela a prouvé que le service peut être créé par le framework DI, mais ensuite, je veux tester le service unitaire, ce qui signifie se moquer des objets injectés.

Comment dois-je procéder?

J'ai essayé de mettre mes objets factices dans le module, par exemple

beforeEach(module(mockNavigationService));

et réécrire la définition de service comme:

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

Mais ce dernier semble arrêter le service créé par la DI comme tout.

Est-ce que quelqu'un sait comment je peux me moquer des services injectés pour mes tests unitaires?

Merci

David

BanksySan
la source
Vous pouvez jeter un oeil à cette réponse à une autre question à moi, je l' espère , il pourrait être utile pour vous.
remigio
Regardez également stackoverflow.com/questions/14238490
jab

Réponses:

183

Vous pouvez injecter des simulations dans votre service en utilisant $provide.

Si vous avez le service suivant avec une dépendance qui a une méthode appelée getSomething:

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

Vous pouvez injecter une version fictive de myDependency comme suit:

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

});

Notez qu'en raison de l'appel, $provide.valuevous n'avez pas besoin d'injecter explicitement myDependency n'importe où. Cela se passe sous le capot lors de l'injection de myService. Lors de la configuration de mockDependency ici, il pourrait tout aussi bien être un espion.

Merci à loyalBrown pour le lien vers cette superbe vidéo .

John Galambos
la source
13
Fonctionne très bien, mais attention à un détail: l' beforeEach(module('myModule'));appel DOIT venir avant l' beforeEach(function () { MOCKING })appel, sinon les simulacres seront écrasés par les vrais services!
Nikos Paraskevopoulos
1
Existe-t-il un moyen de se moquer non pas du service mais constant de la même manière?
Artem
5
Semblable au commentaire de Nikos, tous les $provideappels doivent être effectués avant de l'utiliser, $injectorsinon vous recevrez une erreur:Injector already created, can not register a module!
providencemac
7
Et si votre maquette a besoin de q $? Ensuite, vous ne pouvez pas injecter $ q dans la maquette avant d'appeler module () afin d'enregistrer la maquette. Des pensées?
Jake
4
Si vous utilisez coffeescript et que vous voyez Error: [ng:areq] Argument 'fn' is not a function, got Object, assurez-vous de mettre un returnsur la ligne après $provide.value(...). Le retour implicite a $provide.value(...)causé cette erreur pour moi.
yndolok le
4

À mon avis, il n'est pas nécessaire de se moquer des services eux-mêmes. Simulez simplement les fonctions du service. De cette façon, vous pouvez avoir angular injecter vos vrais services comme il le fait dans toute l'application. Ensuite, moquez les fonctions du service si nécessaire en utilisant la spyOnfonction de Jasmine .

Maintenant, si le service lui-même est une fonction et non un objet que vous pouvez utiliser spyOnavec, il existe une autre façon de procéder. J'avais besoin de le faire et j'ai trouvé quelque chose qui fonctionne plutôt bien pour moi. Voir Comment vous moquez-vous du service angulaire qui est une fonction?

dnc253
la source
3
Je ne pense pas que cela réponde à la question. Que se passe-t-il si l'usine du service moqué fait quelque chose de non trivial, comme frapper le serveur pour les données? Ce serait une bonne raison de vouloir s'en moquer. Vous souhaitez éviter l'appel du serveur et créer à la place une version fictive du service avec de fausses données. Se moquer de $ http n'est pas non plus une bonne solution, car vous testez en fait deux services en un seul test, au lieu de tester les deux services séparément. Je voudrais donc réitérer la question. Comment passer un service simulé à un autre service dans un test unitaire?
Patrick Arnesen
1
Si vous craignez que le service n'atteigne le serveur pour obtenir des données, c'est à cela que sert $ httpBackend ( docs.angularjs.org/api/ngMock.$httpBackend ). Je ne suis pas sûr de savoir quelle autre préoccupation dans l'usine du service nécessiterait de se moquer de l'ensemble du service.
dnc253
2

Une autre option pour faciliter la simulation des dépendances dans Angular et Jasmine consiste à utiliser QuickMock. Il peut être trouvé sur GitHub et vous permet de créer des simulations simples de manière réutilisable. Vous pouvez le cloner à partir de GitHub via le lien ci-dessous. Le README est assez explicite, mais j'espère qu'il pourrait aider d'autres personnes à l'avenir.

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

Il gère automatiquement tout le passe-partout mentionné ci-dessus, vous n'avez donc pas à écrire tout ce code d'injection simulé dans chaque test. J'espère que cela pourra aider.

tennisgent
la source
2

En plus de la réponse de John Galambos : si vous voulez simplement simuler des méthodes spécifiques d'un service, vous pouvez le faire comme ceci:

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

});
Allumeur
la source
1

Si votre contrôleur est écrit pour accepter une dépendance comme celle-ci:

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

alors vous pouvez faire un faux someDependencydans un test Jasmine comme ceci:

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();
    }));
});
Codage avec Spike
la source
9
La question concerne les services, qui ne sont pas instanciés dans la suite de tests avec un appel à un service équivalent en tant que $ controller. En d'autres termes, on n'appelle pas $ service () dans un bloc beforeEach, en passant des dépendances.
Morris Singer
1

J'ai récemment publié ngImprovedTesting qui devrait faciliter les tests simulés dans AngularJS.

Pour tester 'myService' (à partir du module "myApp") avec ses dépendances fooService et barService simulées, vous pouvez simplement faire ce qui suit dans votre test Jasmine:

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

Pour plus d'informations sur ngImprovedTesting, consultez son article de blog d'introduction: http://blog.jdriven.com/2014/07/ng-improved-testing-mock-testing-for-angularjs-made-easy/

Emil van Galen
la source
1
Pourquoi a-t-il été voté à la baisse? Je ne comprends pas la valeur du vote négatif sans commentaire.
Jacob Brewer
0

Je sais que c'est une vieille question, mais il existe un autre moyen plus simple, vous pouvez créer une maquette et désactiver l'original injecté à une fonction, cela peut être fait en utilisant spyOn sur toutes les méthodes. voir le code ci-dessous.

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
la source