Injizieren eines Mocks in einen AngularJS-Dienst

114

Ich habe einen AngularJS-Dienst geschrieben und möchte ihn einem Unit-Test unterziehen.

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

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

    return this;
});

In meiner Datei app.js sind folgende Dateien registriert:

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

Ich kann testen, ob der DI als solcher funktioniert:

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

Dies hat bewiesen, dass der Dienst vom DI-Framework erstellt werden kann. Als Nächstes möchte ich den Dienst jedoch einem Unit-Test unterziehen, was bedeutet, dass die injizierten Objekte verspottet werden.

Wie mache ich das?

Ich habe versucht, meine Scheinobjekte in das Modul einzufügen, z

beforeEach(module(mockNavigationService));

und Umschreiben der Service-Definition als:

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

Letzteres scheint jedoch zu verhindern, dass der Dienst wie alle anderen vom DI erstellt wird.

Weiß jemand, wie ich die injizierten Dienste für meine Unit-Tests verspotten kann?

Vielen Dank

David

BanksySan
quelle
Sie können einen Blick auf diese Antwort von mir auf eine andere Frage, ich hoffe , es ist hilfreich für Sie sein könnte.
Remigio
Schauen Sie auch auf stackoverflow.com/questions/14238490
jab

Antworten:

183

Sie können Mocks in Ihren Service einfügen, indem Sie $provide.

Wenn Sie den folgenden Dienst mit einer Abhängigkeit haben, die eine Methode namens getSomething hat:

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

Sie können eine Scheinversion von myDependency wie folgt einfügen:

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

});

Beachten Sie, dass $provide.valueSie myDependency aufgrund des Aufrufs nicht explizit irgendwo einfügen müssen. Dies geschieht unter der Haube während der Injektion von myService. Wenn Sie hier mockDependency einrichten, kann es sich genauso gut um einen Spion handeln.

Vielen Dank an loyalBrown für den Link zu diesem großartigen Video .

John Galambos
quelle
13
Funktioniert prima, aber ein Detail aufgepasst: der beforeEach(module('myModule'));Anruf HAS vor dem kommenden beforeEach(function () { MOCKING })Anruf, oder aber die Mocks durch die realen Leistungen erhalten überschrieben werden!
Nikos Paraskevopoulos
1
Gibt es eine Möglichkeit, nicht den Service, sondern die Konstante auf die gleiche Weise zu verspotten?
Artem
5
Ähnlich wie bei Nikos 'Kommentar müssen alle $provideAnrufe getätigt werden, bevor sie $injectoranderweitig verwendet werden. Sie erhalten eine Fehlermeldung:Injector already created, can not register a module!
providencemac
7
Was ist, wenn Ihr Mock $ q benötigt? Dann können Sie vor dem Aufruf von module () nicht $ q in den Mock einfügen, um den Mock zu registrieren. Irgendwelche Gedanken?
Jake
4
Wenn Sie Coffeescript verwenden und sehen Error: [ng:areq] Argument 'fn' is not a function, got Object, stellen Sie sicher, dass Sie danach ein returnin die Zeile einfügen $provide.value(...). Die implizite Rückgabe $provide.value(...)hat diesen Fehler für mich verursacht.
Yndolok
4

So wie ich es sehe, besteht keine Notwendigkeit, die Dienste selbst zu verspotten. Verspotten Sie einfach die Funktionen des Dienstes. Auf diese Weise können Sie Ihre realen Dienste wie in der gesamten App eckig injizieren lassen. Verspotten Sie dann die Funktionen des Dienstes nach Bedarf mithilfe der Jasmine- spyOnFunktion.

Wenn der Dienst selbst eine Funktion und kein Objekt ist, mit dem Sie arbeiten können spyOn, gibt es einen anderen Weg, dies zu tun. Ich musste das tun und fand etwas, das für mich ziemlich gut funktioniert. Siehe Wie verspotten Sie den Angular-Dienst, der eine Funktion ist?

dnc253
quelle
3
Ich denke nicht, dass dies die Frage beantwortet. Was ist, wenn die Fabrik des verspotteten Dienstes etwas nicht Triviales tut, wie den Server nach Daten zu durchsuchen? Das wäre ein guter Grund, sich darüber lustig machen zu wollen. Sie möchten den Serveraufruf vermeiden und stattdessen eine Scheinversion des Dienstes mit gefälschten Daten erstellen. Das Verspotten von $ http ist ebenfalls keine gute Lösung, da Sie dann tatsächlich zwei Dienste in einem Test testen, anstatt die beiden Dienste einzeln zu testen. Daher möchte ich die Frage noch einmal wiederholen. Wie übergeben Sie einen Mock-Service in einem Unit-Test an einen anderen Service?
Patrick Arnesen
1
Wenn Sie sich Sorgen machen, dass der Dienst den Server für Daten angreift, ist $ httpBackend genau dafür gedacht ( docs.angularjs.org/api/ngMock.$httpBackend ). Ich bin mir nicht sicher, was in der Fabrik des Dienstes sonst noch ein Problem sein würde, bei dem der gesamte Dienst verspottet werden müsste.
dnc253
2

Eine weitere Option, um das Verspotten von Abhängigkeiten in Angular und Jasmine zu vereinfachen, ist die Verwendung von QuickMock. Es ist auf GitHub zu finden und ermöglicht es Ihnen, einfache Mocks auf wiederverwendbare Weise zu erstellen. Sie können es von GitHub über den unten stehenden Link klonen. Die README-Datei ist ziemlich selbsterklärend, kann aber hoffentlich in Zukunft anderen helfen.

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

Es verwaltet automatisch alle oben genannten Boilerplates, sodass Sie nicht bei jedem Test den gesamten Scheininjektionscode ausschreiben müssen. Hoffentlich hilft das.

Tennisagent
quelle
2

Zusätzlich zu John Galambos 'Antwort : Wenn Sie nur bestimmte Methoden eines Dienstes verspotten möchten, können Sie dies folgendermaßen tun:

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

});
Zünder
quelle
1

Wenn Ihr Controller so geschrieben ist, dass er eine Abhängigkeit wie diese berücksichtigt:

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

dann können Sie someDependencyin einem Jasmin-Test wie folgt eine Fälschung machen :

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
quelle
9
Die Frage bezieht sich auf Dienste, die in der Testsuite nicht mit einem Aufruf eines gleichwertigen Dienstes als $ controller instanziiert werden. Mit anderen Worten, man ruft $ service () nicht in einem beforeEach-Block auf und übergibt Abhängigkeiten.
Morris Singer
1

Ich habe kürzlich ngImprovedTesting veröffentlicht, das Mock-Tests in AngularJS einfacher machen soll.

Um 'myService' (aus dem "myApp" -Modul) mit seinen verspotteten fooService- und barService-Abhängigkeiten zu testen, können Sie in Ihrem Jasmine-Test einfach Folgendes tun:

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

Weitere Informationen zu ngImprovedTesting finden Sie in seinem einführenden Blog-Beitrag: http://blog.jdriven.com/2014/07/ng-improved-testing-mock-testing-for-angularjs-made-easy/

Emil van Galen
quelle
1
Warum wurde dies abgelehnt? Ich verstehe den Wert beim Down-Voting ohne Kommentar nicht.
Jacob Brewer
0

Ich weiß, dass dies eine alte Frage ist, aber es gibt noch einen einfacheren Weg: Sie können ein Mock erstellen und das bei einer Funktion injizierte Original deaktivieren. Dies kann mithilfe von spyOn für alle Methoden erfolgen. siehe Code unten.

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
quelle