将模拟注入 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'));
调用 HAS 在beforeEach(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 服务的主要内容,如果未能解决你的问题,请参考以下文章
Angular 混合错误:试图在设置之前获取 AngularJS 注入器