将模拟注入 AngularJS 服务

Posted

技术标签:

【中文标题】将模拟注入 AngularJS 服务【英文标题】:Injecting a mock into an AngularJS service 【发布时间】:2013-01-24 06:14:40 【问题描述】:

我编写了一个 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 创建的服务。

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

谢谢

大卫

【问题讨论】:

你可以看看this我对另一个问题的回答,希望对你有帮助。 也看***.com/questions/14238490 【参考方案1】:

除了John Galambos' answer:如果你只是想模拟出一个服务的特定方法,你可以这样做:

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

);

【讨论】:

【参考方案2】:

我知道这是个老问题,但还有另一种更简单的方法,您可以创建模拟并禁用在一个函数中注入的原始代码,这可以通过在所有方法上使用 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;
    

【讨论】:

【参考方案3】:

您可以使用$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 提供指向that great video 的链接。

【讨论】:

效果很好,但要注意一个细节:beforeEach(module('myModule')); 调用 HASbeforeEach(function () MOCKING ) 调用之前,否则模拟将被真实服务覆盖! 有没有办法以同样的方式模拟不是服务而是常量? 与 Nikos 的评论类似,任何 $provide 调用必须在使用 $injector 之前进行,否则,您将收到错误:Injector already created, can not register a module! 如果你的 mock 需要 $q 怎么办?然后你不能在调用 module() 之前将 $q 注入到模拟中以注册模拟。有什么想法吗? 如果您使用的是 coffeescript 并且您看到的是Error: [ng:areq] Argument 'fn' is not a function, got Object,请确保在$provide.value(...) 后面加上return。隐式返回 $provide.value(...) 对我造成了该错误。【参考方案4】:

另一个有助于在 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
        );
    );
    ....

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

【讨论】:

【参考方案5】:

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

要测试“myService”(来自“myApp”模块)并模拟出其 fooService 和 barService 依赖项,您可以在 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/

【讨论】:

【参考方案6】:

如果你的控制器被写成接受这样的依赖:

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

那么你可以像这样在 Jasmine 测试中伪造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();
    ));
);

【讨论】:

问题是关于服务的,它不会在测试套件中通过调用任何与 $controller 等效的服务来实例化。换句话说,不会在 beforeEach 块中调用 $service() ,而是传入依赖项。【参考方案7】:

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

现在,如果服务本身是一个函数,而不是一个您可以使用spyOn 的对象,那么还有另一种方法可以解决。我需要这样做,并且发现了一些对我来说效果很好的东西。见How do you mock Angular service that is a function?

【讨论】:

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

以上是关于将模拟注入 AngularJS 服务的主要内容,如果未能解决你的问题,请参考以下文章

AngularJS $在服务注入中注入错误

在 AngularJS 单元测试中模拟模块依赖

Angular 混合错误:试图在设置之前获取 AngularJS 注入器

在AngularJS单元测试中模拟模块依赖性

将 AngularJS 对象作为 TypeScript 类注入(缺少方法)

AngularJs将来自单独文件的多个指令注入一个公共模块