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);
});
Related
I'm attempting to test that a value is changed to true after a promise is resolved inside $onInit. I'm following, as best I can, the example in this Stack Overflow question/answer. Here is my code:
class TestCtrl {
constructor(SearchService) {
this.testValue = false;
this.SearchService = SearchService;
}
$onInit() {
this.SearchService.getResults()
.then(function () {
this.testValue = true;
});
}
}
TestCtrl.$inject = ['SearchService'];
And here's the test I'm attempting to run (using mocha, chai, sinon):
it('should work', function() {
ctrl = $componentController('test', {
SearchService: SearchService
}, {});
sinon.stub(SearchService, 'getResults').resolves({response:{data: 'data'}});
ctrl.$onInit();
$rootScope.$apply();
ctrl.testValue.should.equal(true);
});
Should I be testing ctrl.testValue inside a then? Also, is using this example a bad idea because that example doesn't use a component with an $onInit lifecycle hook?
From what I've read, no, "don't use expect inside then in tests." But I'm not so sure based on what I've read elsewhere.
I wouldn't be surprised if I'm missing something obvious in how to test promises (maybe a stub wasn't the way to go?) and/or how to test what happens in the $onInit lifecycle hook.
If the question needs more details, please ask and I'll do my best to add them.
Edit: Checkout you $onInit method:
$onInit() {
this.SearchService.getResults()
.then(function () {
// `this` in anonymous function is reffering to window not the controller instance
this.testValue = true;
});
}
$onInit() {
var self = this;
self.SearchService.getResults()
.then(function () {
self.testValue = true;
});
}
Your example is correct
This is the way to test async code in angularjs - it is tested like synchronous code. Stubs' returning promises are resolved when you execute $rootScope.$apply().
Why it doesn't work
The promise returned from stub.resolves() is not an angular promise. It cannot be triggered to resolve using $rootScope, because it's not a part of angular's world. It's promise resolution queue is tied to something else and hence the need to test like you usually test async code.
Angular doesn't depend on JavaScript's native Promise implementation - it uses a light implementation of Q's promise library that is wrapped in a service called $q
The answer you have quoted uses the same service to create and return a promise from a stub
In order for your code to work - to test like you test synchronous code - you should return a $q promise (By wrapping a value in $q.when(value)) calling $rootScope.$apply() will execute the code in the then block, then proceed with the code below $rootScope.$apply() line.
Here is an example:
it('Sinon should work with angular promises', function () {
var resolved = 'resolved';
var promise = $q.when(resolved);
// Our async function
var stub = sinon.stub().returns(promise);
// Callback to be executed after the promise resolves
var handler = sinon.stub();
stub().then(handler); // async code
// The handler will be called only after $rootScope.$apply()
handler.callCount.should.equal(0);
// triggers digest which will resolve `ready` promises
// like those created with $q.when(), $q.resolve() or those created
// using the $q.defer() and deferred.resolve() was called
// this will execute the code inside the appropriate callback for
// `then/catch/finally` for all promises and then continue
// with the code bellow -> this is why the test is considered `synchronous`
$rootScope.$apply();
// Verify the handler was called and with the expected value
handler.callCount.should.equal(1);
handler.should.have.been.calledWith(resolved);
})
Here it is in action test promise synchronously in angular
For starters, you should read up on how Mocha expects you to test async code.
To start out with the quick bits:
You are on the right path - there are just some bits missing.
Yes you should do your test inside a then.
The example you linked to is fine. Just understand it.
There is absolutely no reason to avoid asserting a test inside a then. In fact, it is usually the only way to assert the resolved value of a promise.
The main problem with your test code is it tries to assert the result before it is available (as promises resolve in a later tick, they are asynchronous).
The main problem with the code you are trying to test is that there is no way of knowing when the init function has resolved.
We can deal with #2 by waiting for the stubbed SearchService.getResults to resolve (as we control the stub in the test), but that assumes too much knowledge of the implementation of onInit, so that is a bad hack.
Instead, we fix the code in TestCtrl, simply by returning the promise in onInit:
//main code / TestCtrl
$onInit() {
return this.SearchService.getResults()
.then(function () {
this.testValue = true;
});
}
Now we can simply wait for any call to onInit to resolve before we test what has happened during its execution!
To fix your test we first add a parameter to the wrapping test function. Mocha will see this and pass in a function that you can call when your test finishes.
it('should work', function(done) {
That makes it an async test. Now lets fix the test part:
ctrl.$onInit().then( () => {
ctrl.testValue.should.equal(true);
done(); // signals to mocha that the test is finished ok
}).catch(done); // pass any errors to the callback
You might find also find this answer enlightening (upvote if it helps you out). After reading it you might also understand why Mocha also supports dropping the done callback by returning a promise from the test instead. Makes for shorter tests:
return ctrl.$onInit().then( () => {
ctrl.testValue.should.equal(true);
});
sinon.stub(SearchService, 'getResults').resolves({response:{data: 'data'}}); is not returning a promise. Use $q.
I would suggest doing this:
ctrl = $componentController('test', {
SearchService: SearchService
}, {});
let deferred =$q.defer();
deferred.resolve({response:{data: 'data'}});
sinon.stub(SearchService, 'getResults').resolves(deferred.promise);
ctrl.$onInit();
$rootScope.$apply();
ctrl.testValue.should.equal(true);
You don't need to test ctrl.testValue inside a then. And generally, I would recommend not assert inside .then() in your specs. The specs will not fail if the promise never gets resolved. That can give you a false sense of security when in reality, your tests are not doing anything. But that's just my opinion.
Your test will pass once the stub returns a promise. Ideally, I would recommend using $httpBackend if the service is making an http call.
Cheers.
I'm using angularJS and I do this:
xxx.then(function (response) {
$scope.x = response.x;
$scope.y = response.y;
}, function (error) {}
);
The response come from server not instantantly. Then when the response come, I want than the scope update my value, but it does that just when I click in some button other so.
In my html I receive the informations so:
<p>{{x}}</p>
<p>{{y}}</p>
Do you know what I'm doing wrong?
This could be an issue with the digest cycle, try doing $scope.$apply() like below :
xxx.then(function (response) {
$scope.x = response.x;
$scope.y = response.y;
$scope.$apply();
}, function (error) {});
In AngularJS the results of promise resolution are propagated
asynchronously, inside a $digest cycle. So, callbacks registered with
then() will only be called upon entering a $digest cycle.
The results of your promise will not be propagated until the next digest cycle. As there is nothing else in your code that triggers the digest cycle, changes are not getting applied immediately. But, when you click on a button , it triggers the digest cycle, due to which the changes are getting applied
Check this for a clear explanation about this.
A .then method may fail to update the DOM if the promise comes from a source other than the $q service. Use $q.when to convert the external promise to a $q service promise:
//xxx.then(function (response) {
$q.when(xxx).then(function (data) {
$scope.x = data.x;
$scope.y = data.y;
}, function (error) {
throw error;
});
The $q service is integrated with the AngularJS framework and the $q service .then method will automatically invoke the framework digest cycle to update the DOM.
$q.when
Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. This is useful when you are dealing with an object that might or might not be a promise, or if the promise comes from a source that can't be trusted.
-- AngularJS $q Service API Reference - $q.when
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?
It seems that promises do not resolve in Angular/Jasmine tests unless you force a $scope.$digest(). This is silly IMO but fine, I have that working where applicable (controllers).
The situation I'm in now is I have a service which could care less about any scopes in the application, all it does it return some data from the server but the promise doesn't seem to be resolving.
app.service('myService', function($q) {
return {
getSomething: function() {
var deferred = $q.defer();
deferred.resolve('test');
return deferred.promise;
}
}
});
describe('Method: getSomething', function() {
// In this case the expect()s are never executed
it('should get something', function(done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
expect(1).toEqual(2);
});
done();
});
// This throws an error because done() is never called.
// Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
it('should get something', function(done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
expect(1).toEqual(2);
done();
});
});
});
What is the correct way to test this functionality?
Edit: Solution for reference. Apparently you are forced to inject and digest the $rootScope even if the service is not using it.
it('should get something', function($rootScope, done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
});
$rootScope.$digest();
done();
});
You need to inject $rootScope in your test and trigger $digest on it.
there is always the $rootScope, use it
inject(function($rootScope){
myRootScope=$rootScope;
})
....
myRootScope.$digest();
So I have be struggling with this all afternoon. After reading this post, I too felt that there was something off with the answer;it turns out there is. None of the above answers give a clear explanation as to where and why to use $rootScope.$digest. So, here is what I came up with.
First off why? You need to use $rootScope.$digest whenever you are responding from a non-angular event or callback. This would include pure DOM events, jQuery events, and other 3rd party Promise libraries other than $q which is part of angular.
Secondly where? In your code, NOT your test. There is no need to inject $rootScope into your test, it is only needed in your actual angular service. That is where all of the above fail to make clear what the answer is, they show $rootScope.$digest as being called from the test.
I hope this helps the next person that comes a long that has is same issue.
Update
I deleted this post yesterday when it got voted down. Today I continued to have this problem trying to use the answers, graciously provided above. So, I standby my answer at the cost of reputation points, and as such , I am undeleting it.
This is what you need in event handlers that are non-angular, and you are using $q and trying to test with Jasmine.
something.on('ready', function(err) {
$rootScope.$apply(function(){deferred.resolve()});
});
Note that it may need to be wrapped in a $timeout in some case.
something.on('ready', function(err) {
$timeout(function(){
$rootScope.$apply(function(){deferred.resolve()});
});
});
One more note. In the original problem examples you are calling done at the wrong time. You need to call done inside of the then method (or the catch or finally), of the promise, after is resolves. You are calling it before the promise resolves, which is causing the it clause to terminate.
From the angular documentation.
https://docs.angularjs.org/api/ng/service/$q
it('should simulate promise', inject(function($q, $rootScope) {
var deferred = $q.defer();
var promise = deferred.promise;
var resolvedValue;
promise.then(function(value) { resolvedValue = value; });
expect(resolvedValue).toBeUndefined();
// Simulate resolving of promise
deferred.resolve(123);
// Note that the 'then' function does not get called synchronously.
// This is because we want the promise API to always be async, whether or not
// it got called synchronously or asynchronously.
expect(resolvedValue).toBeUndefined();
// Propagate promise resolution to 'then' functions using $apply().
$rootScope.$apply();
expect(resolvedValue).toEqual(123);
}));
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();
});