-
-
Notifications
You must be signed in to change notification settings - Fork 698
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to test an ES7 async function #415
Comments
btw. The only thing that seems to work right now is catching the error via it(`should throw when too short`, async (done) => {
try {
await testuser.password('1a');
done('failed');
} catch (e) {
done();
}
}); But this is ugly and doesn't even use the bdd methods or profit from chai's fluent pattern. |
@krnlde I can't see any reason why that code wouldn't work. What happens if you change the test to the following? it(`should throw when too short`, (done) => {
testuser.password('1a').then(done, done);
}); You should see the test fail appropriately with the error, and not timeout. If it times out - its likely that your code isn't working like it should. It it fails with an error, then it is likely something wrong is happening within either chai or chai-as-promised. |
Same issue as mentioned before. The error is not thrown into the Promise, but in the
The |
The test I wrote should error as expected above. The log you pasted looks right to me. You could try the following: it(`should throw when too short`, () => {
return testuser.password('1a').then(() => throw Error('should error'), () => {})
}); This should pass the test suite without error. If it does then we know mocha and babel aren't the problem. |
When copy pasting your code I get a syntax error at If I wrap the error in curlies... it(`should throw when too short`, () => {
return testuser.password('1a').then(() => {throw Error('should error')}, () => {})
}); ...the promise never settles and mocha skips after the timeout default of 2000ms. If I do: it.only(`should throw when too short`, (done) => {
testuser.password('1a').then(function () {
console.log('fullfilled');
done();
}, function () {
console.log('rejected');
done();
});
}); The |
More investigation done. Facebook's regenerator runtime definitely returns a new Promise when calling an So from my understanding - since we have a Promise that is returned - these assertions both should work: // expect-style
it.only(`should throw when too short`, () => {
return expect(testuser.password('1a')).eventually.throw();
});
// or in should-style
it.only(`should throw when too short`, () => {
return testuser.password('1a').should.eventually.throw();
}); But they don't. Maybe it has sth to do with this line here https://github.com/facebook/regenerator/blob/master/runtime.js#L122 Any thoughts? |
What happens if you try the following: it(`should throw when too short`, (done) => {
testuser.password('1a').should.eventually.throw().notify(done);
}); Also what versions of all software are you using? babel, chai, mocha, chai-as-promised, everything. |
I get
Your code transformed in the expect-style (
as always. What I'm curious about is that testuser.password('1a')
.then(() => console.log("Shouldn't appear"))
.catch(() => console.log("All good")) resolves to An extract of my package.json ( "devDependencies": {
"babel": "latest",
"chai": "latest",
"chai-as-promised": "latest",
"gulp": "latest",
"gulp-eslint": "latest",
"gulp-less": "latest",
"gulp-open": "latest",
"gulp-plumber": "latest",
"gulp-run": "latest",
"gulp-sourcemaps": "latest",
"mocha": "latest",
"run-sequence": "latest"
} Which resolves to: babel@5.0.8, chai@2.2.0, chai-as-promised@4.3.0 and mocha@2.2.1 at the time of writing this. I'm running io.js v1.6.3 on a Win8.1 Pro x64. |
I built a runnable.com instance with the test case so you can see what I see http://runnable.com/VSAUjLYcbqoxDfiY |
I got the solution! Expecting Here's the modified test case: it.only(`should be rejected when too short`, () => {
return expect(testuser.password('1a')).to.be.rejectedWith(Error);
});
// should-style
it.only(`should be rejected when too short`, () => {
return testuser.password('1a').should.be.rejectedWith(Error);
}); The question is, does an |
Oh, duh. Of course! Glad you found the issue @krnlde
|
Done. Thanks for your help @keithamus ! |
Cool. I was wrong too. Thanks for tips. it('electron-prebuilt should be installed', async () => {
await access('node_modules/electron-prebuilt')
})
it('no `electron` should be installed', async () => {
// access('node_modules/electron').should.throw() // wrong
access('node_modules/electron').should.be.rejected()
}) |
@krnlde This solution is incorrect imo. The whole point of using async/await in es7 is so that you can write code without "thinking in promises." That is, you write:
It makes async programming "just like normal again". An assertion library that supports es7 should allow for this style of programming as well. |
Hi @jonahx, thanks for sharing your thoughts, but actually Chai does support the ES7 spec, but your example doesn't really reflect how the spec works. But let's get to the ES7 spec:
So that's how the whole thing happens, you can't really use the example you have just described because In a nutshell: due to how the spec works your example does not match how the feature works. Please let me know if you think I'm wrong or if you need further explanation. Thanks again for sharing your thoughts 😄 |
Hi @lucasfcosta, Thanks for your reply. At this point I see a few possibilities:
To clarify, please try this example out in the babel playground: async function test() {
return Promise.reject('boom!');
}
async function main() {
try {
const result = await test();
console.log(result);
}
catch (e) {
console.log(e);
}
}
main(); This logs "boom!" as expected given the mental model I explained in my previous post. Please let me know how I'm wrong, if you still think I am. Thanks. |
Also, just for full clarification, here's how I'm currently working around the issue in my own code: async function doesItThrow(fn) {
var threwError = true;
try {
await fn();
threwError = false;
} catch (e) { }
return threwError;
}
global.assert.throwsAsync = async (fn, msg) => assert(await doesItThrow(fn), msg);
// and in the test themselves....
await assert.throwsAsync(
async () => await doSomething(missingParams),
'Should error when missing params'
); I thought it was strange that I had to write that myself. |
@jonahx I do stuff like this frequently: it("someTest", async () => {
let err = "_PRETEST_";
try {
await someFn();
} catch (e) {
err = e;
}
expect(err.message).to.equal("some error message");
}); What is it you're asking for? A builtin Chai assertion to handle the try/catch part under the hood, so all you need to do is provide the function? If so, what is it about the previous solution of using it("some test", () => {
return expect(someFn()).to.be.rejectedWith(Error);
}); |
@meeber, Yes, that's what I'm suggesting. What I dislike about |
@jonahx I understand your point now. Well, since we're dealing with promises behind the curtains I think it wouldn't be much of a problem to handle that using We could indeed add that to our core since the only modification we need to handle I'm not sure about whether this would be a good choice or not, so let me ping @keithamus, @vieiralucas and @shvaikalesh so we can get some extra input on this. Thanks again for your response, let's keep on talking about it so we can figure out which is the optimal decision for this issue. 😄 |
@jonahx Agreed on all points. As @lucasfcosta alluded to, we're in a tricky spot currently in terms of adding promise-related features, as Chai currently doesn't have any promise support builtin due to the robustness of chai-as-promised. For now, I recommend creating a new issue for this. Would be useful to see exact examples of how you envision the syntax being if it was Chai-supported. |
Another option that I see is to add an alias to the |
I also agree that Chai should eventually support promises more "natively". So, as @meeber suggested, can you create a new issue for this explaining how do you think Chai should support it? |
FYI I did propose folding chai-as-promised into the chai org a while back (chaijs/chai-as-promised#105 (comment)). Having said that given the direction of async functions - I wonder if perhaps we even need to have any code to support them, or if it becomes supported implicitly by JS features? Perhaps if we want to continue this discussion in earnest, we should move it to a new issue. |
I like @lucasfcosta's idea of adding support for async functions in Given the following function: function throwsAsync() {
return new Promise((resolve, reject) => {
reject(new Error())
})
} I personally find the following syntax: expect(async () => await throwsAsync()).to.throw() much more expressive of intent in an expect(throwsAsync()).to.be.rejected |
I've been trying this method for testing ES7 async functions that are expected to throw an error:
Is this a reasonable usage of chai for expectation of error? Or should I be using a more explicit and readable method? |
@jeff3yan I think that's a reasonable approach. |
@jeff3yan How would you assert on the type/message of your error using that approach? |
@workflow You could do the error assertion in the catch block? |
@jeff3yan You are right of course, I must've been tired. Thank you! |
A sample solution: it('should throw an error', async () => {
await expect(shouldThrowAnError()).to.be.rejected;
}); |
yes, as stated in the original answer here. |
There doesn't appear to be a solution analogous to But the import * as assertRejected from 'assert-rejected';
//...
it("reports failure resulting in exception", () => {
return assertRejected(promiseToTry.then(err => {
assert.instanceOf(err, KindOfError);
});
}); |
This discussion has been going on awhile, and yet after reading it I don't understand: why can't this be really, really simple? If Chai just added one new assertion type, // How to Use
const { expect } = require('chai');
const throwsAsync = () => { return Promise.reject('fake error') };
expect(async () => { await throwsAsync() }).to.throwAsync();
// Synchronous equivalent has (almost) the same signature:
// expect(() => { throw new Error('fake error'); } ).to.throw();
// How it Works
const throwsAsync = async functionToTest => {
try {
await functionToTest();
} catch (err) {
// do what Chai normally does with a sync error
expect(() => { throw err }).to.throw();
return;
}
// do what Chai normally does when no error is thrown
expect(() => {}).to.throw();
}
// How it Works (ES 2015)
const throwsAsync = functionToTest => {
return functionToTest()
.then(() =>
// do what Chai normally does when no error is thrown
expect(() => {}).to.throw();
)
.catch(err =>
// do what Chai normally does with a sync error
expect(() => { throw err }).to.throw();
)
}
} I would be happy to submit a PR of the above, except with real code for the actual assertion definition. |
@machineghost In the example below, wouldn't Chai need Promise-handling logic in every step of the chain after expect(myAsyncFn).to.throwAsync().and.have.property('code', 42); |
I must confess I'm ignorant to the inner workings of Chai, so to support " But if for some reason it is difficult or impossible to make Chai proper fully support a basic part of the language (I think it's safe to call promises "a basic part of the language" at this point, wouldn't you?), I would counter that an:
in Chai proper, which has the limitation of not being chain-off-able, is still infinitely better than not having support for promises/ |
I used Code: // index.js
async function a() {
throw new Error('I am always unhappy!');
};
module.exports.a = a; Test: // index_test.js
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const expect = chai.expect;
const index = require('./index.js');
it('should catch thrown error', () => {
expect(() => index.a()).to.be.rejected;
}); Test output:
|
@Eggham Would this be because you're passing a function to expect rather than the promise itself. I think it would only work on a promise. Which is the result of actually calling the function which returns you a promise (in your case Maybe try changing it('should catch thrown error', () => {
expect(() => index.a()).to.be.rejected;
}); to it('should catch thrown error', () => {
expect(index.a()).to.be.rejected;
}); I believe the syntax you're using (passing a function that calls a function) is used for asserting thrown errors in synchronous functions. Since this isn't synchronous it won't ever "throw" it'll only reject. |
@leggsimon Yeah! You are right. I apparently barked on a wrong tree. Thank you very much! |
So, I found behavior that I can not understand. The code below should fail, yet it passes. async function throwAsyncError () {
throw new Error('Bad stuff happened.')
}
it.only('throws an error', async () => {
expect(throwAsyncError()).to.not.be.rejected;
}); When I add a it.only('throws an error', async () => {
return expect(throwAsyncError()).to.not.be.rejected;
}); It fails like it should. Any explanations? |
@AbdelrahmanHafez This is just how promises work. Consider this example: async function throwAsyncError () {
throw new Error('Bad stuff happened.')
}
it.only('throws an error', async function myTestFn () {
Promise.reject(Error("This rejected promise is swallowed."));
Promise.reject(Error("This rejected promise is also swallowed."));
throwAsyncError(); // This rejected promise is also swallowed
expect(throwAsyncError()).to.not.be.rejected; // This rejected promise is also swallowed
}); The above test passes even though there are four rejected promises inside of it. You can reject as many promises as you want inside of an To fix the problem, you can either: a) add |
async function throwAsyncError () {
throw new Error('Bad stuff happened.')
}
it('throws an error', async () => {
await throwAsyncError().catch((err) => {
(() => { throw err; }).should.throw();
});
try {
await throwAsyncError();
} catch (err) {
(() => { throw err; }).should.throw();
}
}); So the way without edited, if not obsessed with await throwAsyncError().catch(err => err.should.have.property('message', 'Bad stuff happened.')); |
|
@snoblenet You probably meant |
No, The inverse, when you are expecting a result not an error, is:
|
Oh, I didn't notice the You may want to add some more context to both snippets, how |
Thanks @aguegu ! I've been trying to find a workaround for testing async functions that throw errors, and yours have really helped me. |
you can write a function to swap resolve & reject handler, and do anything normally const promise = new Promise((resolve, rejects) => {
YourPromise.then(rejects, resolve);
})
const res = await promise;
res.should.be.an("error"); |
Hey guys,
I use ES7 async functions and tried to debug them. Having this method to test:
and this test case:
...the test case does not catch the error thrown - instead the case succeeds first and fails later (asynchronously) with an uncaught error because the method threw in the scope of the
it()
function not theexpect()
.Any suggestions or advice?
Thanks in advance!
P.S.: Also I created a stackoverflow issue for this a few days ago, but now answer so far. That's why I am calling you guys. http://stackoverflow.com/questions/29334775/how-to-test-an-es7-async-function-using-mocha-chai-chai-as-promised
The text was updated successfully, but these errors were encountered: