I've read about the new warnings regarding Promises not returning from handlers and had a related question...
In some of my unit tests, I stub out the functionality of certain dependencies to return promises using Jasmine's spyOn function. So with this new change, I am seeing quite a few warnings when my tests run. I can of course disable the warnings, but I wondered if there was an improved approach I could use that would naturally get rid of such errors?
Example:
beforeEach(function (done) {
var formatter = new Formatter();
var promise = Promise.resolve(['1,000', '2.45']);
spyOn(formatter, 'format').and.returnValue(promise);
// internally calls formatter.format()
doStuff(formatter, [1000, 2.4567]).then(done);
// promise is not returned
});
Can add a full working plunker if that would be of use?
The issue is that the done callback of jasmine does not return anything, so when you do .then(done) you trigger bluebird's warning for a promise was created in a handler but none were returned from it.
I solved this by adding a new method to the Promise object:
var Promise = require('bluebird');
Promise.prototype.jasmineDone = function(done) {
return this.then(function() {
done();
return null;
}, function(err) {
done.fail(err);
return null;
});
};
Then I refactor my specs from (which triggers the warning):
it('should ...', function(done) {
doSomething().then(done, done.fail);
});
To:
it('should ...', function(done) {
doSomething().jasmineDone(done);
});
Related
Im trying to unit test a function that calls a promise...
Using Mocha, Sinon. I have a functional block like this:
myfile.js:
let OuterDependecy = require('mydep');
function TestFunction(callback) {
OuterDependency.PromiseFunction().then(response => {
//some logic here
}).catch(err => {callback(err)});
inside my test i have used proxyquire to mock the outerdependecy
testfile.js
let proxyquire = require('proxyquire');
let OuterDepStub = {};
let testingFunc = proxyquire('myfile.js', {'mydep': OuterDepStub});
... then inside my testing block
let stubCallback = function() {
console.log('Stub dubadub dub'); //note...i can use sinon.spy here instead
};
beforeEach(()=>{
OuterDependency.PromiseFunction = function(arg) {
return new Promise((resolve, reject)=>{
reject('BAD');
});
};
spy = sinon.spy(stubCallback);
});
my actual test now calls the main "testfunction"
it('Catches Errors, and calls back using error', done => {
TestFunction(stubCallback);
expect(spy).to.have.been.called;
done();
});
I see the stub being called (the console log, hence why i didnt want to use sinon.spy) but the spy is saying its not called. and unit test fails.
I believe this is probably due to a race condition of sorts where the promise is resolving after my test is run... is there anyway to delay the test until my promise is resolve.
i know in in angularjs promise testing, there was a way to "tick" the promise so it resolves when you want to. possible in nodejs?
is there anyway to delay the test until my promise is resolve.
As far as I understand your issue, yes, you should only call done() after the promise is settled. In order to do that,you need two things:
1- Enforce TestFunction to return a Promise, so you can wait until it resolves:
function TestFunction(callback) {
return OuterDependency.PromiseFunction().then(response => {
//some logic here
}).catch(err => { callback(err) });
}
2- Wait to that promise to settle, then call done.
it('Catches Errors, and calls back using error', done => {
TestFunction(stubCallback).then(() => {
expect(spy).to.have.been.called;
done();
})
});
now, our then block won't run until the catch block within TestFunction, so if the test works as expected (i.e. the catch block fires and the callback gets fired), the expectation and the done calls will always fire after the callback gets called.
I see the stub being called (the console log, hence why i didnt want to use sinon.spy) but the spy is saying its not called. and unit test fails.
That's because your expectation runs right after the TestFunction calls, without waiting for it to settle. However, it will get called lately, thus the console.log appears in the next spec.
I'm trying to write a test in JavaScript, the method I'm testing makes 2 method calls (model.expandChildren() and view.update();)
// inside 'app' / 'RV.graph'
var expandChildren = function(){
return model.expandChildren().then(function(r) {
view.update(r);
});
};
I've tried to use Jasmine specs to write the test to spyOn both the view and model functions, but it appears you can only have 1 spy in a given test. It seems I'm missing something big here and that there should be a way to mock out multiple methods calls using spies since my function needs to make both of these calls.
I want my spec to be able to run the way it is below, but it currently only passes the first test (the first spy runs as expected), the second test fails because Jasmine is trying to run the actual function, not the spied function:
var model = GRAPH.model;
var view = GRAPH.view;
var app = RV.graph;
describe('#expandChildren', function() {
beforeEach(function() {
// first spy, all good
spyOn(model, 'expandChildren').and.callFake(function() {
return new Promise(function(resolve) {
resolve(testResponse);
});
});
// second spy doesn't work because Jasmine only allows 1
spyOn(view, 'update');
app.expandChildren();
});
// passing test
it('calls model.expandChildren', function() {
expect(model.expandChildren).toHaveBeenCalled();
});
// failing test that runs the REAL view.update method
it('calls view.update', function() {
expect(view.update).toHaveBeenCalled();
});
});
Is there a way to do this with Jasmine?
Remember that you are working with asynchronous calls. The first call is synchronous, so it is recorded, but the second one only happens later. Give yourself some control over when things happen. I commonly use a pattern like this:
describe('#expandChildren', function() {
var resolver;
it('calls model.expandChildren', function(done) {
spyOn(model, 'expandChildren').and.callFake(function() {
return new Promise(function(resolve) {
resolver = resolve;
});
});
spyOn(view, 'update');
app.expandChildren();
expect(model.expandChildren).toHaveBeenCalled();
expect(view.update).not.toHaveBeenCalled();
resolver();
done();
expect(view.update).toHaveBeenCalled();
});
});
This way, the spec will only be run after the promise has been resolved and done() has been called.
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);
}));
Chai as Promised documentation states as follows:
Notice: either return or notify(done) must be used with promise assertions.
And the examples on the site are as follows:
return doSomethingAsync().should.eventually.equal("foo");
doSomethingAsync().should.eventually.equal("foo").notify(done);
The thing is; I actually wrote a test using chai as promised without returning the promise. Like so:
it('should resolve user', function () {
$state.get(state).resolve.user(dataservice, {
userId: testUser.id
}).should.eventually.eq(testUser);
$rootScope.$apply();
});
And it works perfectly fine. I am sure it does as I change testUser to something else the test fails. Just like I expected. So I am not sure if I am doing something wrong here.
In fact, when I modified the code to return a promise, it failed with error "Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test." The modified code is below:
it('should resolve user', function () {
var promise = $state.get(state).resolve.user(dataservice, {
userId: testUser.id
}).should.eventually.eq(testUser);
$rootScope.$apply();
return promise;
});
A little confused here. It might have something to do with Angular $q. To make it clear, the function resolve.user returns a $q promise.
In the case above Mocha chains returned promise after $rootScope.$apply() was called, so chained then needs another $rootScope.$apply() to be executed. Without this the rest of promise chain is not executed and results in timeout.
Returning promises in Mocha specs is intended for asynchronous specs, this is necessary for testing non-Angular promises. $q promises are synchronous and tied to Angular digests.
As shown here, chai-as-promised can be modified to support $q promises and apply $rootScope.$apply() automatically to asserted promises:
chaiAsPromised.transferPromiseness = function (assertion, promise) {
assertion.then = promise.then.bind(promise);
if (!('$$state' in promise))
return;
inject(function ($rootScope) {
if (!$rootScope.$$phase)
$rootScope.$digest();
});
};
I'm using Mocha to test an asynchronous function that returns a promise.
What's the best way to test that the promise resolves to the correct value?
Mocha has built-in Promise support as of version 1.18.0 (March 2014). You can return a promise from a test case, and Mocha will wait for it:
it('does something asynchronous', function() { // note: no `done` argument
return getSomePromise().then(function(value) {
expect(value).to.equal('foo');
});
});
Don't forget the return keyword on the second line. If you accidentally omit it, Mocha will assume your test is synchronous, and it won't wait for the .then function, so your test will always pass even when the assertion fails.
If this gets too repetitive, you may want to use the chai-as-promised library, which gives you an eventually property to test promises more easily:
it('does something asynchronous', function() {
return expect(getSomePromise()).to.eventually.equal('foo');
});
it('fails asynchronously', function() {
return expect(getAnotherPromise()).to.be.rejectedWith(Error, /some message/);
});
Again, don't forget the return keyword!
Then 'returns' a promise which can be used to handle the error. Most libraries support a method called done which will make sure any un-handled errors are thrown.
it('does something asynchronous', function (done) {
getSomePromise()
.then(function (value) {
value.should.equal('foo')
})
.done(() => done(), done);
});
You can also use something like mocha-as-promised (there are similar libraries for other test frameworks). If you're running server side:
npm install mocha-as-promised
Then at the start of your script:
require("mocha-as-promised")();
If you're running client side:
<script src="mocha-as-promised.js"></script>
Then inside your tests you can just return the promise:
it('does something asynchronous', function () {
return getSomePromise()
.then(function (value) {
value.should.equal('foo')
});
});
Or in coffee-script (as per your original example)
it 'does something asynchronous', () ->
getSomePromise().then (value) =>
value.should.equal 'foo'