-
Notifications
You must be signed in to change notification settings - Fork 779
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
Async: Make tests Promise-aware #634
Conversation
Why does this always add an assertion? |
What would you propose as an alternative? We need to know if the Promise was fulfilled or rejected and reflect such in the test results. |
Are you thinking more like a fulfilled Promise doesn't show up at all but a rejected Promise calls I'm not a huge fan of assertions that only show up in the UI conditionally (e.g. |
context.assert.ok( true, "Promise should be fulfilled" ); | ||
QUnit.start(); | ||
}, | ||
function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems unfortunate to ignore the rejection reason. When QUnit tests throw, do you ignore the exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I'll update that.
You could push a failure or only add the assertion for a rejected promise. The important part being that |
Hmm... thoughts on @scottgonzalez's comments, @stefanpenner / @domenic? |
@JamesMGreene I agree with @scottgonzalez's suggestion, as a rejection detected here is the same as a unexpected |
@@ -72,13 +74,38 @@ Test.prototype = { | |||
this.callbackStarted = now(); | |||
|
|||
if ( config.notrycatch ) { | |||
this.callback.call( this.testEnvironment, this.assert ); | |||
this.callbackRuntime = now() - this.callbackStarted; | |||
promise = this.callback.call( this.testEnvironment, this.assert ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we can to be pedantic (with little cost) we can make the promise usage more spec aligned by not accessing then
more then once.
if (promise !==null && typeof promise == 'object') {
var then = promise.then;
if (type then === 'function') {
then.call(promise, ....);
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, can do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the fulfillment callback be executed with the Promise as the context (this
) as well? Not important for the QUnit code but was just curious for the unit test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you point me to the spec that describes the check you posted? I've looked at this site, but that says that the promise itself can be an object and a function, which doesn't match your suggested approach.
That said, I'd like to simplify this, since we expect tests to only return undefined
and promises, nothing else:
promise = this.callback.call( this.testEnvironment, this.assert );
if ( promise && typeof promise.then === "function" ) {
[...]
}
Updated:
|
Updated:
|
Rebased. |
BTW, @stefanpenner: does Ember have any need for the jQuery/QUnit team folks: do you think |
@@ -72,13 +74,50 @@ Test.prototype = { | |||
this.callbackStarted = now(); | |||
|
|||
if ( config.notrycatch ) { | |||
this.callback.call( this.testEnvironment, this.assert ); | |||
this.callbackRuntime = now() - this.callbackStarted; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you remove this line? Afaik its still needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accidental, thanks for catching that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
Updated to restore the accidentally deleted line that @jzaefferer pointed out. |
promise, | ||
QUnit.start, | ||
function( e ) { | ||
context.pushFailure( "Promise rejected on test #" + ( context.assertions.length + 1 ) + " " + context.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First of all, context.stack
should be e.stack
, right?
Even then, outputting both e.stack
and the result of extractStacktrace( e, 0 )
looks like a bad idea to me - I don't think two stacktraces at once are useful. Though I would like to see an example of the actual output to confirm that suspicion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a duplicate of the standard line (with this
changed to context
) from the existing catch
block a few lines further down.
this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test#stack
is from src/core.js#L111
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, that makes sense. Should've looked at the full file.
Outstanding Question: Should the I'm thinking yes. |
this.callbackRuntime = now() - this.callbackStarted; | ||
if ( promise !== null && typeof promise === "object" ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I posted this before, but it got lost in an update:
Can someone point me to the spec that describes the check @stefanpenner posted? I've looked at this site, but that says that the promise itself can be an object or a function, which doesn't match the suggested approach.
That said, I'd like to simplify this, since we expect tests to only return undefined
and promises, nothing else:
promise = this.callback.call( this.testEnvironment, this.assert );
if ( promise && typeof promise.then === "function" ) {
[...]
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the only part of the spec I'm trying to follow is not access then
property more then once. Your example once again violates this.
That being said, your check does correctly check if promise
itself is a function which my example fails to add, good catch!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for clarifying. The reasoning is explained in this note, which seems odd, but okay to follow.
Since we have to check if promise
is defined first, this still needs to be a bit more complicated then I outlined above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in theory people can implement a custom "then" that has side-effects when invoked. This can be malicious, in-error, or some other funkyness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
TODO:
|
yes |
Updated. Please re-review, thanks! |
Sorry, rebased. |
// Return a mock self-fulfilling Promise ("thenable") | ||
var thenable = { | ||
then: function( fulfilledCallback /*, rejectedCallback */ ) { | ||
assert.ok( true, "`then` function was called" ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's drop this one, since there's an assertion here anyway. This kind of marker ("I was here!") is only useful when there's nothing else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, will do.
Overall this is looking good. I'd like to reduce the amount of seemingly repetitive tests. Any of those that don't actually increase code coverage should be deleted. We should also figure out how to do some "real world testing" with this. Build the dist file and put it into some Ember tests or whatever might already be using something similar. The current tests don't even use "real" Promises, so its hard to tell what we might be missing. |
test = this; | ||
if ( promise != null ) { | ||
then = promise.then; | ||
if ( typeof then === "function" ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I be preferring QUnit.objectType( then ) === "function"
here rather than using the raw JS? I don't see any benefit to that [when checking for a function
] other than consistency with other parts of the library but we're pretty inconsistent across the board right now.
@@ -0,0 +1,74 @@ | |||
// NOTE: Adds 1 assertion | |||
function createMockPromise( assert ) { | |||
// Return a mock self-fulfilling Promise ("thenable") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing empty line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
So I think one potential issue may be that the global |
Added a 2nd separate commit to deal with the synchronization of the global I think I'd rather see them extracted to separate |
@stefanpenner: This one is taking much more persistence than initially expected! You're going to owe me. 😉 |
Created Issue #647 to keep track of getting that synchronization issue resolved. After that, I should be able to undo/clean this 2nd commit. |
Waiting on the resolution of PR #650, then I'll update this PR. |
Now deferring for PR #653 to be merged first as I think it may resolve some of the new test failures I'm getting on this branch as well. |
This PR is now built on top of pending/unmerged PR #653. Doing so does indeed eliminate the 3 unexpected test failures that recently popped up in this branch. You can isolate just the changes necessary for this particular PR (making tests Promise-aware) by viewing this comparison: |
Rebased after merging PR #653. |
LGTM, merging |
At long last... victory! ✌️ 😀 |
Fixes #632.
Ref #534.
I'm not loving how un-DRY theTest#run
function is... but that's not new, only exacerbated by this PR.Updated: This makes the
setup
,teardown
, andtest
methods all aware that their return value may be a Promise in need of asynchronous resolution (resolve
-ing).Criticism or other suggestions welcomed, as usual.
cc: @domenic @stefanpenner