Skip to content
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

Merged
merged 1 commit into from
Sep 11, 2014
Merged

Async: Make tests Promise-aware #634

merged 1 commit into from
Sep 11, 2014

Conversation

JamesMGreene
Copy link
Member

Fixes #632.
Ref #534.

I'm not loving how un-DRY the Test#run function is... but that's not new, only exacerbated by this PR.

Updated: This makes the setup, teardown, and test 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

@scottgonzalez
Copy link
Contributor

Why does this always add an assertion?

@JamesMGreene
Copy link
Member Author

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.

@JamesMGreene
Copy link
Member Author

Are you thinking more like a fulfilled Promise doesn't show up at all but a rejected Promise calls this.pushFailure rather than this.assert.whatever?

I'm not a huge fan of assertions that only show up in the UI conditionally (e.g. assert.expect). 👎

context.assert.ok( true, "Promise should be fulfilled" );
QUnit.start();
},
function() {
Copy link
Contributor

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?

Copy link
Member Author

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.

@scottgonzalez
Copy link
Contributor

You could push a failure or only add the assertion for a rejected promise. The important part being that expect() should only include the assertions the user is implementing. For the failure case, I don't think it's terrible if the count is wrong because we've added an assertion, but if it's not necessary then we shouldn't add it.

@JamesMGreene
Copy link
Member Author

Hmm... thoughts on @scottgonzalez's comments, @stefanpenner / @domenic?

@stefanpenner
Copy link
Contributor

@JamesMGreene I agree with @scottgonzalez's suggestion, as a rejection detected here is the same as a unexpected throw in the test body of synchronous code.

@@ -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 );
Copy link
Contributor

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, can do.

Copy link
Member Author

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

Copy link
Member

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" ) {
  [...]
}

@JamesMGreene
Copy link
Member Author

Updated:

  • No longer adding an assert
  • Including rejection reason in the failure output

@JamesMGreene
Copy link
Member Author

Updated:

@JamesMGreene
Copy link
Member Author

Rebased.

@JamesMGreene
Copy link
Member Author

BTW, @stefanpenner: does Ember have any need for the setup and teardown methods to be Promise-aware as well?

jQuery/QUnit team folks: do you think setup and teardown should be Promise-aware as well?

@@ -72,13 +74,50 @@ Test.prototype = {
this.callbackStarted = now();

if ( config.notrycatch ) {
this.callback.call( this.testEnvironment, this.assert );
this.callbackRuntime = now() - this.callbackStarted;
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

@JamesMGreene
Copy link
Member Author

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 ) );
Copy link
Member

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.

Copy link
Member Author

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

Copy link
Member Author

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

Copy link
Member

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.

@JamesMGreene
Copy link
Member Author

Outstanding Question: Should the setup/teardown methods be Promise-aware as well?

I'm thinking yes.

this.callbackRuntime = now() - this.callbackStarted;
if ( promise !== null && typeof promise === "object" ) {
Copy link
Member

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" ) {
  [...]
}

Copy link
Contributor

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!

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

@JamesMGreene
Copy link
Member Author

TODO:

  • Extract code around the new Promise-aware code into a central, DRY location
  • Update the Test#setup and Test#teardown methods to also be Promise-aware

@Krinkle Krinkle changed the title Async: Tests are now Promise-aware Async: Make tests Promise-aware Aug 25, 2014
@leobalter
Copy link
Member

Should the setup/teardown methods be Promise-aware as well?

yes

@JamesMGreene
Copy link
Member Author

Updated. Please re-review, thanks!

@JamesMGreene
Copy link
Member Author

Sorry, rebased.

// Return a mock self-fulfilling Promise ("thenable")
var thenable = {
then: function( fulfilledCallback /*, rejectedCallback */ ) {
assert.ok( true, "`then` function was called" );
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, will do.

@jzaefferer
Copy link
Member

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" ) {
Copy link
Member Author

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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing empty line

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@JamesMGreene
Copy link
Member Author

So I think one potential issue may be that the global beforeEach/afterEach should really be allowed to fully synchronize before the module-level beforeEach/afterEach is called, otherwise race conditions may be introduced.

@JamesMGreene
Copy link
Member Author

Added a 2nd separate commit to deal with the synchronization of the global beforeEach/afterEach callbacks before running the module-level beforeEach/afterEach calls but I'm not too fond of it... especially as it actually would still be an issue for async scenarios other than Promises.

I think I'd rather see them extracted to separate synchronize calls in the run method rather than lumped together into the test.before/test.after method via test.hooks, which is a realistic bug not covered by PR #635.

@JamesMGreene
Copy link
Member Author

@stefanpenner: This one is taking much more persistence than initially expected! You're going to owe me. 😉

@JamesMGreene
Copy link
Member Author

Created Issue #647 to keep track of getting that synchronization issue resolved. After that, I should be able to undo/clean this 2nd commit.

leobalter added a commit to leobalter/qunit that referenced this pull request Sep 3, 2014
@JamesMGreene
Copy link
Member Author

Waiting on the resolution of PR #650, then I'll update this PR.

@JamesMGreene
Copy link
Member Author

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.

@JamesMGreene
Copy link
Member Author

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:
    async-done...promise-aware

@JamesMGreene
Copy link
Member Author

Rebased after merging PR #653.

@leobalter
Copy link
Member

LGTM, merging

@JamesMGreene
Copy link
Member Author

At long last... victory! ✌️ 😀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Allow tests to return a Promise
6 participants