chai-as-promised tests don't work with $q promises - angularjs

I'm trying to get chai-as-promised to work with $q promises with karma unit tests.
svc.test = function(foo){
if (!foo){
// return Promise.reject(new Error('foo is required'));
return $q.reject(new Error('foo is required'));
} else {
// get data via ajax here
return $q.resolve({});
}
};
it.only('should error on no foo', function(){
var resolvedValue = MyServices.test();
$rootScope.$apply();
return resolvedValue.should.eventually.be.rejectedWith(TypeError, 'foo is required');
});
The unit test just times out. I am not sure what I'm doing wrong here to get the promise to resolve properly. It seems to be an issue with using $q -- when I use native Promise.reject() it works fine.
I filed a ticket here, but nobody seems to be responding:
https://github.com/domenic/chai-as-promised/issues/150

The way chai-as-promised expects to modify promise assertions is transferPromiseness method.
By default, the promises returned by Chai as Promised's assertions are
regular Chai assertion objects, extended with a single then method
derived from the input promise. To change this behavior, for instance
to output a promise with more useful sugar methods such as are found
in most promise libraries, you can override
chaiAsPromised.transferPromiseness.
For Angular 1.3+ support, $q promises can be duck-typed by $$state property, so native promises won't be affected:
chaiAsPromised.transferPromiseness = function (assertion, promise) {
assertion.then = promise.then.bind(promise);
if (!('$$state' in promise))
return;
inject(function ($rootScope) {
if (!$rootScope.$$phase)
$rootScope.$digest();
});
};
chaiAsPromised chains each asserted promise with then. Even if the promise is settled, the rest of the chain still requires the digest to be triggered manually with $rootScope.$digest().
As long as the spec contains no asynchronous code, it becomes synchronous, no promise is required to be returned:
it('...', () => {
...
expect(...).to.eventually...;
expect(...).to.eventually...;
});
And is equal to mandatory $rootScope.$digest() after each set of eventually assertions/expectation when transferPromiseness wasn't set:
it('...', () => {
...
expect(...).to.eventually...;
expect(...).to.eventually...;
$rootScope.$digest();
});

You need to change the order of execution in your tests. Asynchronous tasks with chai-as-promised need to happen before the expectation.
it('does not work', () => {
$timeout.flush();
expect(myAsyncTask()).to.eventually.become('foo');
})
it('does work', () => {
expect(myAsyncTask()).to.eventually.become('foo');
$timeout.flush();
})
You need to initiate the call to the asynchronous task before flushing the queue of asynchronous tasks.
Also, don't use $rootScope.$digest. That may have other side effects that are not desirable within your test(s).
$timeout.flush is what you're looking for.
https://docs.angularjs.org/api/ngMock/service/$timeout
To get your specific test(s) working:
it('should error on no foo', function(){
MyServices.test().should.eventually.be.rejectedWith(TypeError, 'foo is required')
$rootScope.$apply();
});
it('should pass on foo', function(){
MyServices.test('foo').should.eventually.become({});
$rootScope.$apply();
}
tl;dr
it('async test', () => {
setup();
expect();
execute();
})
it('sync test', () => {
setup();
execute();
expect();
})
Given the comments posted:
Should it be mentioned that it is unethical to downvote 'rival' answers on the question you're answering?
Fair enough. I think the answer is misleading, given that there is no extra setup necessary to get chai-as-promised working with Angular without having to deal with the done callback. Fwiw, I'll go ahead and try to revoke said downvote and be ethical about it.
The OP has no signs of timeout in his code and doesn't state that the task is asynchronous. $rootScope.$digest() has no side effects in specs when called outside of scope digest. The reason why it is not recommended in production is because it doesn't have the safeguards that $apply has.
$rootScope.$digest is effectively the same as $rootScope.$apply (and $scope.$apply for that matter). source
$timeout.flush will flush non-$timeout based functions just as well. It is not exclusive to $timeout based functions.
Plunker to showcase how it just works™:
plunker

Related

Delay testing of a value until a promise has resolved

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.

Angularjs Mocha test $q promises without $rootScope.$apply

I have this service:
angular.module('domeeApp')
.factory('streamWidget', streamWidgetFactory);
function streamWidgetFactory($q) {
return {
loadContent: function() {
return $q(function(resolve, reject) {
resolve('test');
})
}
}
}
I'm testing it with karma/mocha/chai:
describe('streamWidget', function() {
beforeEach(module('domeeApp'));
var streamWidget;
var $timeout;
beforeEach(inject(function(_$timeout_, _streamWidget_) {
streamWidget = _streamWidget_;
$timeout = _$timeout_;
}));
it('should load new content', function(done) {
streamWidget.loadContent()
.then(function(res) {
expect(res).to.equal('test');
done();
})
.catch(function(){})
$timeout.flush();
});
});
Since $q promises doesn't work well with mocha i'm following this answer, which says to add $timeout.flush() to force the .then method of the promise to be executed.
The problem is, after calling .flush(), all my app wakes up and i start to get this errors from angular-mocks:
Error: Unexpected request: GET /partials/page/view/index.
I know about $httpBackend, but it would be insane to mock ALL the requests my app is making on startup.
Is there a way to make $q promises work with mocha without calling $timeout.flush() or $rootScope.$apply()?
As shown here, chai-as-promised can be used to assert $q promises.
With this setup
chaiAsPromised.transferPromiseness = function (assertion, promise) {
assertion.then = promise.then.bind(promise);
if (!('$$state' in promise))
return;
inject(function ($rootScope) {
if (!$rootScope.$$phase)
$rootScope.$digest();
});
};
digest cycles will be triggered automatically on promise assertions, executing the whole promise chain.
In this case the spec
it('...', () => {
...
expect(...).to.eventually...;
expect(...).to.eventually...;
$rootScope.$digest();
});
can omit $digest() call and become
it('...', () => {
...
expect(...).to.eventually...;
expect(...).to.eventually...;
});
Notice that $q promises are synchronous, they shouldn't be returned from Mocha spec or call done callback.
Here's an alternative strategy that we use because we never actually need $httpBackend, but it sometimes (randomly) fails making requests for templates used by directives (even though those templates are available in $templateCache):
beforeEach(function() {
module('app', function($provide) {
// This is using jasmine, but the idea is the same with mocha.
// Basically just replace $httpBackend with a function that does nothing.
$provide.constant('$httpBackend', jasmine.createSpy('$httpBackend'));
});
});
Of course, if you actually use $httpBackend in other cases, then this won't work, as you'll need it to mock response objects.

Getting "$digest already in progress" in async test with Jasmine 2.0

I know that calling $digest or $apply manually during a digest cycle will cause a "$digest already in progress" error but I have no idea why I am getting it here.
This is a unit test for a service that wraps $http, the service is simple enough, it just prevents making duplicate calls to the server while ensuring that code that attempts to do the calls still gets the data it expected.
angular.module('services')
.factory('httpService', ['$http', function($http) {
var pendingCalls = {};
var createKey = function(url, data, method) {
return method + url + JSON.stringify(data);
};
var send = function(url, data, method) {
var key = createKey(url, data, method);
if (pendingCalls[key]) {
return pendingCalls[key];
}
var promise = $http({
method: method,
url: url,
data: data
});
pendingCalls[key] = promise;
promise.then(function() {
delete pendingCalls[key];
});
return promise;
};
return {
post: function(url, data) {
return send(url, data, 'POST');
},
get: function(url, data) {
return send(url, data, 'GET');
},
_delete: function(url, data) {
return send(url, data, 'DELETE');
}
};
}]);
The unit-test is also pretty straight forward, it uses $httpBackend to expect the request.
it('does GET requests', function(done) {
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(function(result) {
expect(result.data).toEqual('The response');
done();
});
$httpBackend.flush();
});
This blows up as sone as done() gets called with a "$digest already in progress" error. I've no idea why. I can solve this by wrapping done() in a timeout like this
setTimeout(function() { done() }, 1);
That means done() will get queued up and run after the $digest is done but while that solves my problem I want to know
Why is Angular in a digest-cycle in the first place?
Why does calling done() trigger this error?
I had the exact same test running green with Jasmine 1.3, this only happened after I upgraded to Jasmine 2.0 and rewrote the test to use the new async-syntax.
$httpBacked.flush() actually starts and completes a $digest() cycle. I spent all day yesterday digging into the source of ngResource and angular-mocks to get to the bottom of this, and still don't fully understand it.
As far as I can tell, the purpose of $httpBackend.flush() is to avoid the async structure above entirely. In other words, the syntax of it('should do something',function(done){}); and $httpBackend.flush() do not play nicely together. The very purpose of .flush() is to push through the pending async callbacks and then return. It is like one big done wrapper around all of your async callbacks.
So if I understood correctly (and it works for me now) the correct method would be to remove the done() processor when using $httpBackend.flush():
it('does GET requests', function() {
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(function(result) {
expect(result.data).toEqual('The response');
});
$httpBackend.flush();
});
If you add console.log statements, you will find that all of the callbacks consistently happen during the flush() cycle:
it('does GET requests', function() {
$httpBackend.expectGET('/some/random/url').respond('The response');
console.log("pre-get");
service.get('/some/random/url').then(function(result) {
console.log("async callback begin");
expect(result.data).toEqual('The response');
console.log("async callback end");
});
console.log("pre-flush");
$httpBackend.flush();
console.log("post-flush");
});
Then the output will be:
pre-get
pre-flush
async callback begin
async callback end
post-flush
Every time. If you really want to see it, grab the scope and look at scope.$$phase
var scope;
beforeEach(function(){
inject(function($rootScope){
scope = $rootScope;
});
});
it('does GET requests', function() {
$httpBackend.expectGET('/some/random/url').respond('The response');
console.log("pre-get "+scope.$$phase);
service.get('/some/random/url').then(function(result) {
console.log("async callback begin "+scope.$$phase);
expect(result.data).toEqual('The response');
console.log("async callback end "+scope.$$phase);
});
console.log("pre-flush "+scope.$$phase);
$httpBackend.flush();
console.log("post-flush "+scope.$$phase);
});
And you will see the output:
pre-get undefined
pre-flush undefined
async callback begin $digest
async callback end $digest
post-flush undefined
#deitch is right, that $httpBacked.flush() triggers a digest. The problem is that when $httpBackend.verifyNoOutstandingExpectation(); is run after each it is completed it also has a digest. So here's the sequence of events:
you call flush() which triggers a digest
the then() is executed
the done() is executed
verifyNoOutstandingExpectation() is run which triggers a digest, but you are already in one so you get an error.
done() is still important since we need to know that the 'expects' within the then() are even executed. If the then doesn't run then you might now know there were failures. The key is to make sure the digest is complete before firing the done().
it('does GET requests', function(done) {
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(function(result) {
expect(result.data).toEqual('The response');
setTimeout(done, 0); // run the done() after the current $digest is complete.
});
$httpBackend.flush();
});
Putting done() in a timeout will make it executes immediately after the current digest is complete(). This will ensure that all of the expects that you wanted to run will actually run.
Adding to #deitch's answer. To make the tests more robust you can add a spy before your callback. This should guarantee that your callback actually gets called.
it('does GET requests', function() {
var callback = jasmine.createSpy().and.callFake(function(result) {
expect(result.data).toEqual('The response');
});
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(callback);
$httpBackend.flush();
expect(callback).toHaveBeenCalled();
});

How to resolve promises in AngularJS, Jasmine 2.0 when there is no $scope to force a digest?

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);
}));

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