将模拟注入 AngularJS 服务

IT技术 javascript angularjs mocking jasmine angularjs-service
2021-01-19 12:28:06

我编写了一个 AngularJS 服务,我想对其进行单元测试。

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

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

    return this;
});

我的 app.js 文件注册了这些:

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

我可以测试 DI 是否正常工作:

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

这证明服务可以由 DI 框架创建,但是接下来我想对服务进行单元测试,这意味着模拟注入的对象。

我该怎么做?

我试过将我的模拟对象放在module中,例如

beforeEach(module(mockNavigationService));

并将服务定义重写为:

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

但后者似乎停止了由 DI 创建的服务。

有谁知道我如何为我的单元测试模拟注入的服务?

谢谢

大卫

6个回答

您可以使用$provide.

如果您有以下具有依赖项的服务,该服务具有名为 getSomething 的方法:

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

您可以按如下方式注入 myDependency 的模拟版本:

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

});

请注意,由于对$provide.value的调用实际上并不需要在任何地方显式注入 myDependency。它在注入 myService 期间发生在幕后。在这里设置 mockDependency 时,它很容易成为间谍。

感谢loyalBrown提供该精彩视频的链接

有没有办法以同样的方式模拟而不是服务而是恒定的?
2021-03-13 12:28:06
如果您使用的是 coffeescript 并且看到了Error: [ng:areq] Argument 'fn' is not a function, got Object,请确保return$provide.value(...). 隐式返回$provide.value(...)给我造成了那个错误。
2021-03-16 12:28:06
如果你的模拟需要 $q 怎么办?然后你不能在调用 module() 之前将 $q 注入到模拟中以注册模拟。有什么想法吗?
2021-03-26 12:28:06
伟大的作品,但要注意一个细节:beforeEach(module('myModule'));呼叫HAS来的前beforeEach(function () { MOCKING })调用,否则嘲笑将被覆盖在真正的服务!
2021-03-29 12:28:06
与 Nikos 的评论类似,任何$provide调用都必须在使用之前进行,$injector否则,您将收到错误消息:Injector already created, can not register a module!
2021-04-05 12:28:06

在我看来,没有必要嘲笑服务本身。只需模拟服务上的功能。这样,您就可以像在整个应用程序中一样使用 angular 注入您的真实服务。然后,根据需要使用 Jasmine 的spyOn函数模拟服务上的函数。

现在,如果服务本身是一个函数,而不是一个您可以使用的对象,那么spyOn还有另一种方法来处理它。我需要这样做,并找到了一些对我来说效果很好的东西。请参阅如何模拟作为函数的 Angular 服务?

如果您担心服务会访问服务器以获取数据,这就是 $httpBackend 的用途(docs.angularjs.org/api/ngMock.$httpBackend)。我不确定服务工厂中还有什么需要模拟整个服务的问题。
2021-03-22 12:28:06
我不认为这回答了问题。如果被模拟的服务的工厂做了一些重要的事情,比如点击服务器获取数据怎么办?这将是想要嘲笑它的一个很好的理由。您想避免服务器调用,而是使用虚假数据创建服务的模拟版本。模拟 $http 也不是一个好的解决方案,因为这样您实际上是在一个测试中测试两个服务,而不是单独对两个服务进行单元测试。所以我想再次重申这个问题。如何在单元测试中将模拟服务传递给另一个服务?
2021-03-24 12:28:06

在 Angular 和 Jasmine 中帮助简化模拟依赖项的另一个选择是使用 QuickMock。它可以在 GitHub 上找到,并允许您以可重用的方式创建简单的模拟。您可以通过以下链接从 GitHub 克隆它。README 是非常自我解释的,但希望它将来可以帮助其他人。

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

它会自动管理上面提到的所有样板文件,因此您不必在每个测试中都写出所有模拟注入代码。希望有帮助。

除了John Galambos 的回答:如果您只想模拟服务的特定方法,您可以这样做:

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

});

如果您的控制器被编写为接受这样的依赖项:

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

那么你可以someDependency像这样在 Jasmine 测试中做一个假

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();
    }));
});
问题是关于服务的,它不会在测试套件中通过调用任何等效的服务作为 $controller 来实例化。换句话说,不会在 beforeEach 块中调用 $service() ,而是传入依赖项。
2021-04-03 12:28:06