When unit testing a service that usings $resource, what is best practice - using $httpBackend or mocking the resource?
I have the following service:
angular.module('example')
.factory('MyService', ['$resource',
function($resource) {
var service = $resource('/api/example/', {}, {
create: {
method: 'POST'
}
});
var create = function(payload) {
return service.create({}, payload).$promise;
};
return {
create: create
};
}
]);
Using $httpbackend
describe('#create', function() {
it('should send a post request to api/example', function() {
$httpBackend.expectPOST('/api/example')
.respond({
name: 'Something'
});
MyService.create({ example: 'payload' }});
$httpBackend.flush();
expect(MyService.create).toEqual({example: 'payload'})
});
});
When you are testing the service, use $httpBackend. It's designed as a stand-in for the usual $http module, so you can guarantee your service's call through $resource and back does the right thing.
If you were to mock $resource, you wouldn't be exercising nearly as much of the code path. You would also be entangling your tests with the implementation of your service, which could conceivably switch from using $resource to using $http directly, or some third module. Your test doesn't care how the HTTP call is made, just that it's the right one and it returns some expected data.
When you're testing some part of your system other than the service, mock away. In that case, you only need MyService.create to return a particular object, and you don't care how it gets it. There's no reason to tie non-service tests to the HTTP call the service needs to make.
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
I am trying to do a simple mock of angular's $http with sinon in a Mocha test.
But my spy never has any results in it no matter what I try.
searchResource.typeAhead is my function under test. It calls $http based on its arguments and I want to make sure the request is correct.
searchResource.typeAhead returns a promise, but I tried putting the checking code in .then() and it never executes.
suite('Search Resource', function() {
var injector = angular.injector(['cannonball-client-search', 'cannonball-client-core']);
var searchResource = injector.get('SearchResource');
suite('#typeAhead()', function () {
setup(function () {
this.server = sinon.fakeServer.create();
this.server.respondWith('GET',
config.endpoints.search.typeAhead,
[200, {'Content-Type': 'application/json'}, '[{ "id": 12, "comment": "Hey there" }]']);
this.spyhttp = sinon.spy(injector.get('$http'));
});
teardown(function () {
this.server.restore();
});
test('positive', function (done) {
searchResource.typeAhead(
'expl',
[{entityType: 'itementity'}],
[{createdBy: 'Eric'}, {createdBy: 'Tal'}],
10
);
this.server.respond();
expect(this.spyhttp.calledWith({
method: 'get',
url: config.endpoints.search.typeAhead +
'?query=expl&filter=entityType:itementity&orfilter=createdBy:Eric&orfilter=createdBy:Tal&limit=10'
})).to.be.true();
done();
});
});
});
The problem lies outside of Sinon mocking.
If angular.injector is used directly instead of suggested angular.mock.module and angular.mock.inject helpers, the one is on his own with it and his knowledge of Angular injector.
The obvious downside is that the injector won't be torn down automatically after each spec (while it would be when angular.mock.module is used), so all nested specs operate on the same instance of Angular injector.
At this point
var searchResource = injector.get('SearchResource');
SearchResource service instance was already injected with unmocked $http, that's the end of the story. Even if it wouldn't, there's no chance that Angular will ever know that this.spyhttp spy should be used instead of original $http service. Its methods can be spied after the instantiation
sinon.spy($http, 'get');
but not $http function itself.
The strategy for testing with angular.injector may be
var $httpSpy;
var injector = angular.injector([
'cannonball-client-search',
'cannonball-client-core',
function ($provide) {
$provide.decorator('$http', function ($delegate) {
return ($httpSpy = sinon.spy($delegate));
});
}
]);
// injector.get('$http') === $httpSpy;
Notice that this will make Sinon spy on $http function, not on its methods.
If the question is about how Angular mocks should be approached with Sinon, then it's as easy as that. Otherwise this may indicate an XY problem, and the other answer addresses it directly ($httpBackend and the way $http embraces it are there exactly to make the burden of mocking XMLHttpRequest requests non-existent).
Angular was built with testing in mind. The previous comments aren't suggesting that you cannot use sinon to mock out $http, its just not common practice and it definitely won't be as easy to do as it is with $httpBackend.
I would personally only be using sinon to mock any dependencies which don't belong to Angular as such. It's easy enough to provide mock responses with $httpBackend:
$httpBackend.when('GET', '/url').respond({
mock: 'response'
});
Now any request to '/url' with use the mock response object. I'm sure $httpBackend has some other complicated wizardry built in to handle other things like interceptors perhaps?
I'm looking to write a Jasmine unit test which executes a callback function passed to a then function. This then function is chained to a call to the AngularJS $http service, and it's inside a custom service. Here's the code I'm working with:
app.service('myService', function($rootScope, $http) {
var service = this;
var url = 'http://api.example.com/api/v1/resources';
service.resources = {
current: []
};
service.insertResource = function (resource) {
return $http.post(url, resource).then(function(response){
$rootScope.$broadcast('resources:updated', service.resources.current);
return response;
});
};
});
Here's my attempt to write a test which executes this callback, but to no avail:
describe('resource service', function() {
beforeEach(angular.mock.module('myapp'));
var resourceService;
beforeEach(inject(function(_resourceService_) {
resourceService = _resourceService_;
}));
it('should insert resources', function() {
resourceService.insertResource({});
});
});
There are several approaches you could take:
Use $httpBackend.expectPOST
Use $httpBackend.whenPOST
Move the code in the callback to a named function (not an anonymous one) and write a test for this function. I sometimes take this route b/c I don't want the trouble of writing tests with $httpBackend. I only test the callback function, I don't test that my service is calling the callback. If you can live w/that it's much simpler approach.
Check the documentation for $httpBackend for details. Here's a simple example:
describe('resource service', function() {
beforeEach(angular.mock.module('myapp'));
var resourceService, $httpBackend;
beforeEach(inject(function($injector) {
resourceService = $injector.get('resourceService');
$httpBackend = $injector.get('$httpBackend');
}));
afterEach(function() {
// tests will fail if expected HTTP requests are not made
$httpBackend.verifyNoOutstandingRequests();
// tests will fail if any unexpected HTTP requests are made
$httpBackened.verifyNoOutstandingExpectations();
});
it('should insert resources', function() {
var data: { foo: 1 }; // whatever you are posting
// make an assertion that you expect this POST to happen
// the response can be an object or even a numeric HTTP status code (or both)
$httpBackend.expectPOST('http://api.example.com/api/v1/resources', data).respond({});
// trigger the POST
resourceService.insertResource({});
// This causes $httpBackend to trigger the success/failure callback
// It's how you workaround the asynchronous nature of HTTP requests
// in a synchronous way
$httpBackend.flush();
// now do something to confirm the resource was inserted by the callback
});
});
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;
});
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.