See this plunkr for a live example: http://plnkr.co/edit/djQPW7g4HIuxDIm4K8RC
In the code below, the line var promise = serviceThatReturnsPromise(); is run during module configuration time, but I want to mock out the promise that is returned by the service.
Ideally I'd use the $q service to create the mock promise, but I can't do that because serviceThatReturnsPromise() is executed during module configuration time, before I can get access to $q. What's the best way to resolve this chicken and egg problem?
var app = angular.module('plunker', []);
app.factory('serviceUnderTest', function (serviceThatReturnsPromise) {
// We mock out serviceThatReturnsPromise in the test
var promise = serviceThatReturnsPromise();
return function() {
return 4;
};
});
describe('Mocking a promise', function() {
var deferredForMock, service;
beforeEach(module('plunker'));
beforeEach(module(function($provide) {
$provide.factory('serviceThatReturnsPromise', function() {
return function() {
// deferredForMock will be undefined because this is called
// when `serviceUnderTest` is $invoked (i.e. at module configuration),
// but we don't define deferredForMock until the inject() below because
// we need the $q service to create it. How to solve this chicken and
// egg problem?
return deferredForMock.promise;
}
});
}));
beforeEach(inject(function($q, serviceUnderTest) {
service = serviceUnderTest;
deferredForMock = $q.defer();
}));
it('This test won\'t even run', function() {
// we won't even get here because the serviceUnderTest
// service will fail during module configuration
expect(service()).toBe(4);
});
});
I'm not sure I like the solution much, but here it is:
http://plnkr.co/edit/uBwsJxJRjS1qqsKIx5j7?p=preview
You need to ensure that you don't instantiate "serviceUnderTest" until after you've set-up everything. Therefore, I've split the second beforeEach into two separate pieces: the first instantiates and uses $q, the second instantiates and uses serviceUnderTest.
I've also had to include the $rootScope, because Angular's promises are designed to work within a $apply() method.
Hope that helps.
Related
I have a controller that calls a Service which is a wrapper for a Resource. Like this:
app.factory("Service", ["$resource", function ($resource) {
return $resource("/api/Service/get");
}]);
Return value of the service's method is assigned to a variable within the controller. Normally, the variable is of type Resource and it contains a promise. When the promise is resolved, the variable is populated with all values returned from the backend.
I track then on the promise in order to modify the model received from the backend. Like so:
this.model = Service.get();
this.model.$promise.then(function(data) {
// do something with data
});
I need to test the value of the resulting model variable in my controller.
The only way I found to do this, is to use $httpBackend with a real implementation of my Service. However, this is ugly because then, testing my controller, I have to pass request path "api/Service/get" to the httpBackend.when() in order for it to respond with some value.
An excerpt form my test:
// call Controller
$httpBackend.when('GET', '/api/Service/get').respond(someData);
$httpBackend.flush();
expect(scope.model.property).toBe(null);
This seems and feels utterly wrong. The whole point of using a separate service to deal with resource is for the controller to not know anything about the url and http method name. So what should I do?
In other words, what I want to test is that then gets called and does what I need it to do.
I guess I could probably create a separate service that gets called in then and do what I need to do with the model but it feels a bit overkill if all I want to do is, for example, set one field to null depending on a simple condition.
You are correct, you shouldn't have to use $httpBackend unless you are using $http in the controller you are testing.
As you wrote, the controller shouldn't need to know anything about the implementation of Service. What the controller knows is that Service has a get method that returns an object with a $promise property that is a promise.
What you want to do is to use a fake implementation of Service in your test. There are multiple ways to do this via mocks, spies, stubs etc, depending on your use case and which testing framework(s) you are using.
One way is to create a fake implementation like this:
var Service = {
get: function() {
deferred = $q.defer();
return {
$promise: deferred.promise
};
}
};
You want to be able to access deferred from the tests, so you can either resolve or reject the promise based on what you want to test.
Full setup:
var $rootScope,
scope,
createController,
$q,
deferred;
var Service = {
get: function() {
deferred = $q.defer();
return {
$promise: deferred.promise
};
}
};
beforeEach(function() {
module('App');
inject(function(_$rootScope_, $controller, _$q_) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
createController = function() {
$controller('MyController', {
$scope: scope,
Service: Service
});
};
$q = _$q_;
});
});
Controller implementation:
app.controller('MyController', function($scope, Service) {
$scope.property = false;
$scope.model = Service.get();
$scope.model.$promise.then(function(data) {
if (data) {
$scope.property = true;
}
});
});
You can then spy on the fake implementation to assert that it is called correctly.
Example with Jasmine:
spyOn(Service, 'get').and.callThrough();
You need and.callThrough() or the call will be interrupted and your fake implementation will not be used.
You now have full control by manually creating the controller, resolving the promise and triggering the digest loop and can test the different states:
it('Should work', function() {
spyOn(Service, 'get').and.callThrough();
expect(Service.get).not.toHaveBeenCalled();
createController();
expect(Service.get).toHaveBeenCalled();
expect(scope.property).toBeFalsy();
deferred.resolve('some data');
$rootScope.$digest();
expect(scope.property).toBeTruthy();
});
Demo: http://plnkr.co/edit/th2pLWdVa8AZWOyecWOF?p=preview
Lets say I have a service which queries some data and sets it in the controller, a little similar to:
(Method on controller)
DogService.query(function(data)){
if(data.isSuccess){
$scope.IloveDogs = true;
$scope.dogLovers += 1;
}
})
It is highly simplified, but how would I in my controller test that when calling a mocked dogService, that it sets the correct data?
If for simplicity we say that the function isn't asynchronous and deals with promises, I would create and inject a mock to the controller. The mock could look like:
var DogService = {
query: function(){
return true;
}
}
This unfortunately doesn't run the code where the $scope.IloveDogs is set to true, and the dogLovers is incremented by one.
Any ideas, since I would rather not have to duplicate the code in my controller from the service to the mocked service?
This is how I would normally mock a service in a unit test.
(You didn't mention which testing framework you use, so I am going to assume Jasmine as it's the most popular one at the moment).
I just create a dumb object to act as my mock and then just Jasmine's built-in spy functionality to dictate what it returns. Note that this is syntax for Jasmine 2.0.
I use $q to create a promise, and make sure I am able to reference it from my tests so I can resolve it.
describe('Spec', function() {
var scope;
var catServiceMock;
var deferredCatCall;
beforeEach(module('myModule'));
beforeEach(inject(function($controller, $rootScope, $q) {
scope = $rootScope;
//Create a mock and spy on it to return a promise
deferredCatCall = $q.defer();
catServiceMock = {
query: function() {}
};
spyOn(catServiceMock, 'query').and.returnValue(deferredCatCall.promise);
//Inject the mock into the controller
$controller('MyCtrl', {
$scope: scope,
catService: catServiceMock
});
}));
it('proves that cats are better than dogs', function() {
//resolve the promise that was returned by the mock
deferredCatCall.resolve({
isSuccess: true
});
//Need to trigger a $digest loop so angular process the resolved promise
scope.$digest();
//Check that the controller callback did something
expect(scope.iLoveCats).toBeTruthy();
});
});
For a service that does not use promises, I would possibly do something like this:
describe('Spec', function() {
var scope;
var catServiceMock;
beforeEach(module('myModule'));
beforeEach(inject(function($controller, $rootScope, $q) {
scope = $rootScope;
//Create a mock and spy on it to return a value
catServiceMock = {
query: function() {}
};
spyOn(catServiceMock, 'query').and.returnValue({
isSuccess: true
});
//Inject the mock into the controller
$controller('MyCtrl', {
$scope: scope,
catService: catServiceMock
});
}));
it('proves that cats are better than dogs', function() {
//Check that the controller callback did something
expect(scope.iLoveCats).toBeTruthy();
});
});
The main problem with this approach is that you're forced to dictate what the service will return before you instantiate the controller. This means that if you want to test how the controller behaves to different data received from the service you're going to have to have multiple beforeEach blocks nested in different describe blocks and while it looks at a glance like it's less boilerplate in the test you will end up with a lot more.
This is one of the reasons why I prefer my services to return promises even if they are not asynchronous.
I am writing unit tests for a controller. This controller has a $resource service injected :
function controller($scope, Service) {
Service.get(function(result){
// do stuff with the result, not relevant here
}
}
The service is defined like this :
angular.module('so').factory('Service', ['$resource', service]);
function service($resource) {
return $resource('/url', null, {
get: { method: 'POST', params: {}, isArray: false}
});
}
My Jasmine unit test is the following :
describe("Controller", function(){
var $httpBackend;
beforeEach(function() {
module('so');
inject(function( _$httpBackend_) {
$httpBackend = _$httpBackend_;
});
});
it('should have done stuff irrelevant to the question', function() {
var $injector = angular.injector('so'),
$scope = $injector.get('$rootScope'),
$httpBackend
.whenPOST('/url')
.respond ([]);
// controller needs to be defined here and not in the beforeEach as there
// are more parameters passed to it, depending on the test
var controller = $injector.get('$controller')('controller', { "$scope": $scope });
$httpBackend.flush();
// then here the actual test resolution, also irrelevant
});
});
I get an error when running the test :
Error: No pending request to flush ! in file:///path/to/angular-mock.js (line 1453)
I added a console.log() in the callback from Service.get() and indeed, it is not called (everything outside of the callback is of course called). Also tried to add a scope digest if not phased after controller creation in the unit test, as I saw suggested in an other question, with no luck.
I know that I can mock that in some other ways, but using $httpBackend seems the perfect solution for the test : mocking the webserver and the data received.
I'm using AngularJS 1.2.16 (can't upgrade to 1.3.*, IE 8 compatibility required). I first used 1.2.13 and updated to check if it would solve the issue, without any luck.
That was an injection issue that was solved by changing the test from
it('should have done stuff irrelevant to the question', function() {
var $injector = angular.injector('so'),
$scope = $injector.get('$rootScope'),
$httpBackend
.whenPOST('/url')
.respond ([]);
// controller needs to be defined here and not in the beforeEach as there
// are more parameters passed to it, depending on the test
var controller = $injector.get('$controller')('controller', { "$scope": $scope });
$httpBackend.flush();
// then here the actual test resolution, also irrelevant
});
To:
it('should have done stuff irrelevant to the question', inject(function(Service) {
// edited lines because they did not change
var controller = $injector.get('$controller')('controller', { "$scope": $scope, "Service": Service });
// edited lines because they did not change
}));
So basicaly, adding the inject() in the test function and passing the service to the controller "manually".
I found the issue, that's great, but I don't really understand why it doesn't work. Also, I tried this right after finding the solution :
it('should have done stuff irrelevant to the question', inject(function() {
// edited lines because they did not change
var Service = $injector.get('Service'),
var controller = $injector.get('$controller')('controller', { "$scope": $scope, "Service": Service });
// edited lines because they did not change
}));
but this fail again, with the same "no pending request" error. I'm guessing that's some sort of racing issue, where my service can't get the proper $httpBackend to be injected when it's created afterwards, but I don't really understand why this occurs. If anybody can enlighten me... I'll be grateful.
All I need to do is to download a json file and assign it to OCategories in PCategory provider after I set the path. However I get an error that $http doesnt exist. How can I inject it into my provider and download inside of the setPath function?
var app = angular.module('NSApp',
[
'ui.bootstrap',
'MDItem',
'MDUser',
'MDNotification',
'MDUpload'
]
);
app.config(function(PCategoriesProvider)
{
PCategoriesProvider.setPath('data/csv/categories.json');
});
MDItem/provider/category.js
angular.module('MDItem').provider('PCategories',function(){
var OCategories;
var OPath;
return{
setPath: function(d){
OPath = d;
console.log('Path is set. Trying to download categories.');
OCategories = $http.get(oPath);
},
$get : function() {
return {
categories : OCategories
}
}
}
});
You can never inject service instances into config functions or providers, since they aren't configured yet. Providers exist to configure specific services before they get injected. Which means, there's always a corresponding provider to a certain service. Just to clarify, here's a little example configuring $location service using $locationProvider:
angular.module('myModule').config(function ($locationProvider) {
$locationProvider.html5Mode(true);
});
So what happens here, is that we configure $location service to use its html5mode. We do that by using the interfaces provided by $locationProvider. At the time when config() is executed, there isn't any service instance available yet, but you have a chance to configure any service before they get instantiated.
Later at runtime (the earliest moment ist the run() function) you can inject a service. What you get when injecting a service is what its providers $get() method returns. Which also means, each provider has to have a $get() function otherwise $injector would throw an error.
But what happens, when creating custom services without building a provider? So something like:
angular.module('myModule').factory('myService', function () {
...
});
You just don't have to care about, because angular does it for you. Everytime you register any kind of service (unless it is not a provider), angular will set up a provider with a $get() method for you, so $injector is able to instantiate later.
So how to solve your problem. How to make asynchronous calls using $http service when actually being in configuration phrase? The answer: you can't.
What you can do, is run the $http call as soon as your service gets instantiated. Because at the time when your service get instantiated, you're able to inject other services (like you always do). So you actually would do something like this:
angular.module('myModule').provider('custom', function (otherProvider, otherProvider2) {
// some configuration stuff and interfaces for the outside world
return {
$get: function ($http, injectable2, injectable3) {
$http.get(/*...*/);
}
};
});
Now your custom provider returns a service instance that has $http as dependency. Once your service gets injected, all its dependencies get injected too, which means within $get you have access to $http service. Next you just make the call you need.
To make your this call is getting invoked as soon as possible, you have to inject your custom service at run() phrase, which looks like this:
angular.module('myModule').run(function (custom, injectable2) {
/* custom gets instantiated, so its $http call gets invoked */
});
Hope this makes things clear.
Since all services are singletons in angular you could simply store a variable in a factory with the $http promise. And then when the factory is called at startup it will download the json.
You can then also expose a method on the factory that refreshes the data.
I know this is not the exact answer to your question, but I thought I'd share how I would do it.
angular.module('MDItem').factory('PCategories', function ($http, PCategoriesPath) {
var service = {
categories: [],
get: function () {
if (angular.isUndefined(PCategoriesPath)) {
throw new Error('PCategoriesPath must be set to get items');
}
$http.get(PCategoriesPath).then(function (response) {
service.categories = response.data;
});
}
};
// Get the categories at startup / or if you like do it later.
service.get();
return service;
});
// Then make sure that PCategoriesPath is created at startup by using const
angular.module('MDItem').const('PCategoriesPath', 'data/csv/categories.json');
angular.module('app').controller('myCtrl', function ($scope, PCategories) {
$scope.categories = PCategories.categories;
// And optionally, use a watch if you would like to do something if the categories are updated via PCategories.get()
$scope.$watch('categories', function (newCategories) {
console.log('Look maa, new categories');
}, true); // Notice the true, which makes angular work when watching an array
})
You have to inject $http in the function $get, because that's the function called by the injector.
However, to download the categories you would be better off using promises:
angular.module('MDItem').provider('PCategories',function(){
var OCategories;
var OPath;
return{
setPath: function(d){
OPath = d;
console.log('Path is set');
},
$get : function($http) {
return {
fetch: function () {
var deferred = $q.defer();
$http.get(oPath).then(function (value) {
deferred.resolve(value);
}
return deferred.promise;
}
}
}
}
});
I implemented what I wanted with a diffrent approach which is quite simple and effective. Just add a dummy controller in the main index.html(NOT PARTIAL). Data is now shared between all my modules and controllers and everything is downloaded once. :) Oh I love AJ.
...
<div ng-controller="initController" hidden></div>
...
initController:
angular.module('NSApp',[]).controller("initController",function($scope, $http, FCategory, FLocation){
$http.get('data/json/categories.json').then(function (response) {
FCategory.categories = response.data;
});
$http.get('data/json/cities.json').then(function (response) {
FLocation.cities = response.data;
});
$http.get('data/json/regions.json').then(function (response) {
FLocation.regions = response.data;
});
});
And now you can access it:
angular.module('MDTest', []).controller("test",function($scope, FCategory, FLocation){
$scope.categories = FCategory.categories;
FCategory factory
angular.module('MDItem').factory('FCategory', function ($http) {
var service = {
categories: [],
....
};
return service;
});
I have a service, 'Inputs', defined in module 'Puts', that depends on a second service, 'InputCreator'. I need to stub the InputCreator service in order to test the Inputs service.
As I understand the answer here, I should create a module containing my stub service, then create a new 'Test' module, specifying the module under test and then the stub module as dependencies. And then pull the service from the injector. Like so:
beforeEach(function() {
angular.module.('Puts'); // contains the service 'Inputs'
angular.module('Mocks',[])
.service('InputCreator',function(){
var mockInputs = {
//stubbed behaviour goes here
};
return mockInputs;
});
});
angular.module('Test',['Puts', 'Mocks'];
inject(function($injector){
Inputs = $injector.get('Inputs');
});
});
However, the injector function responds with 'unknown InputsProvider <- Inputs'.
Where have I gone astray?
Thanks!
Having figured this out, I thought I'd answer my own question. The big mistake above was using angular.module rather than angular.mock.module, that is convenience referenced as module by angular-mock. They aren't the same thing at all!
Additionally, it's enough to initialize the mock service with angular.mock.module, so long as you do it before you initialize the module under test. There's no need for this 'wrapping the modules in a third module' business as suggested in the question linked above. To wit:
describe("Test Service", function() {
var TestService, getvaluestub;
beforeEach(function() {
// create mock service
var mock = {getvalue:function(){}}
angular.module('dependencymodule',[])
.service('dependencyservice',function () {
return mock;
});
//mock the function we are stubbing, (that, in this case, returns value 4)
getvaluestub = sinon.stub(mock,'getvalue')returns(4);
//instantiate your mock service
module('dependencymodule');
//instantiate the module of the service under test,
//that depends on 'dependencyservice' mocked above
//(ie - testmodule includes the service 'testservice')
module('testmodule');
//inject your test service for testing
inject(function ($injector) {
TestService = $injector.get('testservice');
})
//tests go here.....
If the dependency module already exists, you could either still do all of the above, or you could acquire the service from the $injector, insert your spies and stubs, and >then< instantiate the service under test. It's important that the spies/stubs are set up >before< the dependent service is instantiated, or it will be instantiated without them. It looks like this:
describe("Test Service", function() {
var TestService, DependencyService, getvaluestub;
beforeEach(function() {
// these modules are specified in the application
module('dependencymodule');
module('testmodule');
inject(function ($injector) {
DependencyService = $injector.get('testservice');
getvaluestub = sinon.stub(DependencyService,'getvalue').returns(4);
OtherService = $injector.get('otherservice');
})
});
// test go here
So, there you go. Hopefully this is useful to someone who searches for 'Injecting mocks into angular services'.