将模拟注入AngularJS服务


114

我写了一个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框架创建服务,但是接下来我要对服务进行单元测试,这意味着要模拟注入的对象。

我该怎么做呢?

我试图将我的模拟对象放在模块中,例如

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所创建的所有服务。

有人知道我可以如何为单元测试模拟注入的服务吗?

谢谢

大卫


你可以看看这个回答我的另一个问题,我希望这能对您有所帮助。
remigio

Answers:


183

您可以使用将模拟注入到您的服务中$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的过程中。在此处设置嘲笑依赖时,它很容易成为间谍。

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


13
效果很好,但要提防细节: beforeEach(module('myModule'));呼叫HAS来的前beforeEach(function () { MOCKING })调用,否则嘲笑将被覆盖在真正的服务!
Nikos Paraskevopoulos

1
有没有一种方法可以模拟不是服务而是以相同的方式进行常量化?
Artem

5
与Nikos的评论类似,$provide在使用$injector其他任何电话之前必须先进行任何调用,否则会收到错误消息:Injector already created, can not register a module!
providencemac

7
如果您的模拟需要$ q怎么办?然后,您不能在调用module()之前将$ q注入到模拟中以注册该模拟。有什么想法吗?
2015年

4
如果您正在使用coffeescript并且看到的话Error: [ng:areq] Argument 'fn' is not a function, got Object,请确保return在之后加入一行$provide.value(...)。隐式返回$provide.value(...)对我造成了该错误。
yndolok

4

从我的角度来看,无需自己模拟服务。只需在服务上模拟功能即可。这样,您就可以像在整个应用程序中一样对您的真实服务进行有角度的注入。然后,使用Jasmine的spyOn功能根据需要在服务上模拟功能。

现在,如果服务本身是一个函数,而不是可以使用的对象,那么spyOn还有另一种解决方法。我需要执行此操作,并发现一些对我来说非常有效的方法。请参阅如何模拟作为功能的Angular服务?


3
我认为这不能回答问题。如果要嘲笑的服务工厂发生了一些不平凡的事情,例如击中服务器以获取数据怎么办?那将是一个要嘲笑它的好理由。您希望避免服务器调用,而是使用伪造数据创建服务的模拟版本。模拟$ http也不是一个好的解决方案,因为然后您实际上是在一个测试中测试两个服务,而不是孤立地对两个服务进行单元测试。因此,我想再次重申这个问题。如何在单元测试中将模拟服务传递给另一个服务?
Patrick Arnesen

1
如果您担心该服务会影响服务器中的数据,那就是$ httpBackend的用途(docs.angularjs.org/api/ngMock.$httpBackend)。我不确定在服务的工厂中还有什么需要关注的,这将需要模拟整个服务。
dnc253

2

帮助简化Angular和Jasmine中的模拟依赖项的另一种方法是使用QuickMock。它可以在GitHub上找到,并允许您以可重用的方式创建简单的模拟。您可以通过下面的链接从GitHub克隆它。自述文件很容易说明,但希望将来对其他人有所帮助。

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

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


2

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

});

1

如果您的控制器被编写为采用这样的依赖关系:

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

那么您可以someDependency像这样在茉莉花测试中制造假冒品:

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

9
问题是关于服务的,这些服务不会在测试套件中通过调用$ controller等等效服务来实例化。换句话说,不会在beforeEach块中调用$ service(),而是传递依赖关系。
莫里斯·辛格

1

我最近发布了ngImprovedTesting,它应该使AngularJS中的模拟测试更加容易。

要模拟出其fooService和barService依赖项来测试“ myService”(来自“ myApp”模块),您可以简单地在Jasmine测试中执行以下操作:

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

有关ngImprovedTesting的更多信息,请查看其介绍性博客文章:http ://blog.jdriven.com/2014/07/ng-improved-testing-mock-testing-for-angularjs-made-easy/


1
为什么这被否决了?不加评论,我不理解拒绝投票的价值。
Jacob Brewer 2015年

0

我知道这是一个老问题,但是还有另一种更简单的方法,您可以创建模拟并在一个函数中禁用原始注入,可以通过在所有方法上使用spyOn来完成。参见下面的代码。

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;
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.