How do I mock angular's $http with sinon? - angularjs

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?

Related

How to test an AngularJS controller value that is set within a promise using Sinon

I'm having troubling testing a controller's value that's set within a promise returned by a service. I'm using Sinon to stub the service (Karma to run the tests, Mocha as the framework, Chai for assertions).
I'm less interested in a quick fix than I am in understanding the problem. I've read around quite a bit, and I have some of my notes below the code and the test.
Here's the code.
.controller('NavCtrl', function (NavService) {
var vm = this;
NavService.getNav()
.then(function(response){
vm.nav = response.data;
});
})
.service('NavService', ['$http', function ($http) {
this.getNav = function () {
return $http.get('_routes');
};
}]);
Here's the test:
describe('NavCtrl', function () {
var scope;
var controller;
var NavService;
var $q;
beforeEach(module('nav'));
beforeEach(inject(function($rootScope, $controller, _$q_, _NavService_){
NavService = _NavService_;
scope = $rootScope.$new();
controller = $controller;
}));
it('should have some data', function () {
var stub = sinon.stub(NavService, 'getNav').returns($q.when({
response: {
data: 'data'
}
}));
var vm = controller("NavCtrl", {
$scope: scope,
NavService: NavService
});
scope.$apply();
stub.callCount.should.equal(1);
vm.should.be.defined;
vm.nav.should.be.defined;
});
});
The stub is being called, i.e. that test passes, and vm is defined, but vm.nav never gets data and the test fails. How I'm handling the stubbed promise is, I think, the culprit. Some notes:
Based on reading elsewhere, I'm calling scope.$apply to set the value, but since scope isn't injected into the original controller, I'm not positive that will do the trick. This article points to the angular docs on $q.
Another article recommends using $timeout as what would "actually complete the promise". The article also recommends using "sinon-as-promised," something I'm not doing above. I tried, but didn't see a difference.
This Stack Overflow answer use scope.$root.$digest() because "If your scope object's value comes from the promise result, you will need to call scope.$root.$digest()". But again, same test failure. And again, this might be because I'm not using scope.
As for stubbing the promise, I also tried the sinon sandbox way, but results were the same.
I've tried rewriting the test using $scope, to make sure it's not a problem with the vm style, but the test still fails.
In the end, I could be wrong: the stub and the promise might not be the problem and it's something different and/or obvious that I've missed.
Any help is much appreciated and if I can clarify any of the above, let me know.
Sorry but a quick fix was all that you needed:
var stub = sinon.stub(NavService, 'getNav').returns($q.when({
response: {
data: 'data'
}
}));
Your promise is resolved to object containing response.data not just data
Checkout this plunk created from your code: https://plnkr.co/edit/GL1Xuf?p=preview
The extended answer
I have often fallen to the same trap. So I started to define the result returned from a method separately. Then if the method is async I wrap this result in a promise like $q.when(stubbedResult) this allow me to, easily run expectations on the actual result, because I keep the stubbed result in a variable e.g.
it('Controller should have some data', function () {
var result = {data: 'data'};
var stub = sinon.stub(NavService, 'getNav').returns($q.when(result));
var vm = controller(/* initController */);
scope.$apply();
stub.callCount.should.equal(1);
vm.nav.should.equal(result.data)
})
Also some tests debugging skill will come in handy. The easiest thing is to dump some data on the console just to check what's returned somewhere. Working with an actual debugger is preferable of course.
How to quickly catch mistakes like these:
Put a breakpoint at the $rootScope.apply() line (just before it is executed)
Put a breakpoint in the controller's NavService.getNav().then handler to see whether it is called and what it was called with
Continue with the debugger to execute the $rootScope.$apply() line. Now the debugger should hit the breakpoint set at the previous step - that's it.
I think you should use chai-as-promised
and then assert from promises like
doSomethingAsync().should.eventually.equal("foo");
or else use async await
it('should have some data', async function () {
await scope.$apply();
});
you might need to move then getNav() call in init kinda function and then test against that init function

Testing if a promise is called from a controller unit-testing AngularJS

I am writing unit tests in AngularJS using karma and I would like to test the following controller whether is calling or not the service. I cant use spyOn because it is a promise and not a function. I don't want to test the service itself but I can't figure out how can I do it.
$scope.deleteItem= function(itemId){
service.deleteItem(itemId)
.then(function(){
$scope.info("Deleted");
$scope.getData();
}, function(data){
$scope.critical('error');
});
};
Without more context, I'm not seeing why spyOn wouldn't work. It should. The fact that deleteItem returns a promise doesn't prevent you from using spyOn to spy on the deleteItem function. You should be using spyOn to replace the deleteItem function with a spy, and any value you return with the spy should be a promise.
There may be an issue with with how you're injecting the service into the controller in the test. You would have to post some of your test code to determine if this is the case.
If you have a reference to the service dependency in your test, you should be able to use spyOn to spy on the deleteItem method. Then you just need to provide an appropriate promise as the return value.
To simulate a resolved promise:
spyOn(service, 'deleteItem').and.returnValue($q.when(value));
To simulate a rejected promise:
spyOn(service, 'deleteItem').and.returnValue($q.reject(value));
And in most cases, you'll need a manual call to $rootScope.$digest() somewhere in the test to let the angular lifecycle know it's time to resolve the promise.
After the $rootScope.$digest(), you can verify that your controller took the proper actions when the promise was resolved (or rejected).
it('controller.deleteItem() should do stuff after service.deleteItem completes successfully', function() {
var controller = createController();
spyOn(service, 'deleteItem').and.returnValue($q.when());
controller.deleteItem('id');
//Tell Angular to resolve the promise
$rootScope.$digest();
// Verify that the controller is in correct state now that promise is resolved
// i.e. expect(something).toEqual(somethingElse);
});

Unit testing a service with $resource, $httpbackend vs mocking

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.

AngularJS "No pending request to flush" while unit testing a controller with a $resource

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.

How do I expect an HTTP request NOT to be made?

Angular's $httpBackend service lets you expect an HTTP request with expectGET, expectPOST, etc. (or just expect).
How would I write a test that says, "the controller should NOT make a request to this endpoint (under these conditions)"?
I was thinking something like:
$httpBackend.when('/forbidden/endpoint').respond(function() {
throw Error("Shouldn't be making a request to /forbidden/endpoint!");
});
That seems a bit hacky to me, but I'm fine with it if that's the normal way to do things. (But I doubt that.)
I stumbled over the same issue.
The solution would be to have a callback function as response and inside you could expect(true).toBe(false) or in my opinion something a little bit more beautiful:
it ('should not trigger HTTP request', function() {
var forbiddenCallTriggered = false;
$httpBackend
.when('/forbidden/endpoint')
.respond(function() {
forbiddenCallTriggered = true;
return [400, ''];
});
// do whatever you need to call.
$rootScope.$digest();
$httpBackend.flush();
// Let test fail when request was triggered.
expect(forbiddenCallTriggered).toBe(false);
});
For scenarios like this I often use Jasmine's spyOn() function. You can spy on functions of $http, $resource, or of a custom service (like myServiceThatUsesHTTP below):
spyOn(myServiceThatUsesHTTP, 'query');
// test, then verify:
expect(myServiceThatUsesHTTP.query).not.toHaveBeenCalled();
// or
expect(myServiceThatUsesHTTP.query.callCount).toBe(0);
When you spyOn() a function, the original function is replaced. The code for the original function is not executed, which can be good or bad (depending on what you need to do for the test).
For example, if you need the $promise object that $http or $resource returns, you can do this:
spyOn($http, '$get').andCallThrough();
One solution might be to check if $httpBackend.flush() throws an exception, since there should be nothing to flush:
beforeEach(function() {
$httpBackend.whenGET('/forbidden/endpoint');
...
// call service method under test (that should not make $http call)
});
it('Should not call the endpoint', function() {
expect($httpBackend.flush).toThrow();
});
Important thing to note: we use when and not expect, since we don't actually expect the call to be made. And since there is no call, $httpBackend.flush() will throw an exception: No pending request to flush.
$httpBackend is not applied because the $http call doesn't get made in this test.
Instead, you can inject $http in your test, and then spyOn() $http directly:
beforeEach(fn () {
inject(function ($injector) {
this.service = $injector.get('serviceOrControllerOrDirectiveBeingTested');
this.$http = $injector.get('$http');
}
});
and then
it('should ...', fn() {
spyOn(this.$http, 'get');
this.service.methodThatTriggersTheHttpCall();
expect(this.$http.get).not.toHaveBeenCalled();
});

Resources