Reputation: 15329
Surely this has been asked before but I can't find it. I need to mock a factory, but the mock itself needs to use $q, and I end up in a chicken and egg situation with regards to calling module() after inject().
I looked at this question which advises doing a spyOn, which works for services because it is a singleton, but I am calling new on the function returned by my factory, creating a new instance each time, so that won't work...
var app = angular.module('app', []);
app.factory('MyDependencyFactory', function() {
return function() {
this.doPromise = function () {
var defer = $q.defer();
//obviously more complicated.
defer.resolve();
return defer.promise;
}
}
});
app.factory('MyConsumingFactory', function(MyDependencyFactory) {
return function() {
var dependency = new MyDependencyFactory();
this.result;
this.doSomething = function () {
dependency.doPromise().then(
function (data) {
this.result = data;
},
function (error) {
console.log(error);
}
);
}
}
});
Jasmine test:
describe('MyConsumingFactory', function() {
var MyConsumingFactory;
beforeEach(function () {
module('app');
inject( function (_MyConsumingFactory_) {
MyConsumingFactory = _MyConsumingFactory_;
});
inject( function ($q) {
mockMyDependencyFactory = function () {
this.doPromise = function (data) {
var defer = $q.defer();
defer.resolve('mock data');
};
};
});
module( function ($provide) {
$provide.factory('MyDependencyFactory', mockMyDependencyFactory);
});
});
it('works correctly', function () {
MyConsumingFactory.doSomething();
$rootScope.$apply();
expect(MyConsumingFactory.result).toEqual('mock data');
});
});
I need my mockMyDependencyFactory to use $q, so I need to wrap it in inject( function(..., and I need to do this before the call to module( function ($provide) {... which of course give me:
Error: Injector already created, can not register a module!
Any suggestions on how I get round this?
Or if you think my design flawed (I suppose I could instantiate a MyDependencyFactory and pass that during instantiation of MyConsumingFactory instead of using angular's DI?) I'm all ears :)
Upvotes: 1
Views: 1180
Reputation: 13319
First of all, all your calls to module()
should be before inject()
, otherwise you will get this error: Injector already created, can not register a module!
i.e. you should register modules before you inject them in code. Knowing that, we need to mock MyDependencyFactory
before injecting, but how do we get $q
in there if it is only available in inject()
? Actually, it is a common technique in angular tests, to assign injected service to a global variable in a test suite, and then use it across all scenarios:
describe('some suite', function () {
// "global" variables for injected services
var $rootScope, $q;
beforeEach(function () {
module('app');
module(function($provide) {
$provide.factory('MyDependencyFactory', function () {
return function () {
this.doPromise = function (data) {
// use "globals"
var defer = $q.defer();
defer.resolve('mock data');
return defer.promise;
};
};
});
});
inject(function (_$rootScope_, _$q_) {
// assign to "globals"
$rootScope = _$rootScope;
$q = _$q;
});
});
// ....
});
The reason you can use $q
in a $provide
block is that it is not being used immediately, it will be used only when you call a mocked method or create an instance of a mocked object. By that time, it will be injected and assigned to a global variable $q
and have an appropriate value.
One more trick you could do if you want to resolve your promise with different values several times, is to create a global variable defer
and initialize it not inside specific method, but in some beforeEach
block, and then do defer.resolve('something')
inside your scenario with a value you want in this particular scenario.
Here you can see a working example of your code, I've made some extra fixes to make it work (has comments).
Note: I am saying "global" variable, but it is not actually global as in JS terminology, but global within a particular test suite.
Upvotes: 5