-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Overhaul test implementation 👷🏗 #1314
Conversation
Don't make the duration comparison so precise, since timers may fire early.
* Clarify responsibilities * Consistently import dependencies * Clarify significance of `exports.avaRequired` * Stop mimicking process with process-adapter. Reference it using `adapter` instead. Use the `process` global where applicable. Masking the process global with a mimicked object is unnecessarily confusing. * Remove superstitious delays in exiting workers The worker now only exits when told by the main process. This means the IPC channel must have drained before the main process can send the instruction. There's no need to wait before sending the message that teardown has completed. The AppVeyor workaround was introduced to solve Node.js 0.10 issues. We're no longer supporting that version. In theory, issues around flushing I/O exist regardless of whether AVA is running in AppVeyor. There is no clear way around this though, so let's assume it's not actually an issue.
Asynchronous `t.throws()` / `t.notThrows()` was the only case where internal AssertionErrors were leaked to user code. Return `undefined` instead.
Simplify Runner by passing options to TestCollection.
There's no need to collect all results. Tests emit them directly.
Test results are emitted directly, so Test#run() only needs to return a (promise for) a boolean, indicating whether the test passed. Make the same change in Concurrent and Sequence, and return a boolean if all runnables passed. Refactor tests to stop relying on Test instances returning their result.
* Do away with underscore prefixes. They were used inconsistently, and are generally not worth it * Keep `_test` prefixed in ExecutionContext, since that is exposed to calling code * Reorder methods * Assume all arguments are passed and are correct. They're already validated in `test-collection.js` *Pass metadata when instantiating Tests * Rewrite test finish logic. There's still a fair bit of interleaving due to the `test.cb()` API, but it's now a lot easier to understand. Due to the last change, tests with `t.end()` and only synchronous assertions end immediately. Previously they would end asynchronously due to a promise being in the completion chain. Similarly, returning a promise or observable for a `test.cb()` test immediately fails.
Though currently this error is likely to get lost unless there is a pending assertion or `test.cb()` is used.
Need to update the type definitions to take this into account. |
That said, it would make this example quite annoying: const err = await t.throws(promise)
t.is(err.message, 'foo') Because if const err = await t.throws(promise)
if (err) {
t.is(err.message, 'foo')
} And yes the code will crash without that guard, but that error is ignored given the original assertion failure in |
I don't get it. test(async t => {
const err = await t.throws(Promise.reject(new Error('foo')));
console.log(err.message);
}); The test(async t => {
const err = await t.throws(Promise.resolve(new Error('foo')));
console.log(err.message);
}); What will change? |
It'll no longer throw, just like |
I see. I use the pattern of assigning |
@@ -0,0 +1,6 @@ | |||
'use strict'; | |||
|
|||
const test = require('../../..'); |
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.
These fixtures are transpiled with Babel, so you can drop the 'use strict';
and use import
.
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.
Yea I can clean that up. I probably just copied these from elsewhere.
lib/sequence.js
Outdated
|
||
const result = this.tests[i].run(); | ||
for (let next = iterator.next(); !next.done; next = iterator.next()) { |
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 not just a for-of
loop instead of extracting the iterator?
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.
Because of promises. The alternative is maintaining an index
variable to resume the iteration. That's what iterators do all by themselves though.
lib/test-collection.js
Outdated
@@ -124,14 +125,14 @@ class TestCollection extends EventEmitter { | |||
context = null; | |||
} | |||
|
|||
return new Test(hook.metadata, title, hook.fn, context, this._emitTestResult); | |||
return new Test(hook.metadata, title, hook.fn, false, context, this._emitTestResult); |
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.
Time to use an object for the arguments?
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.
Yea I was having my doubts about this. On the one hand it's an internal API so hey, but on the other it's a lot of arguments…
@@ -673,6 +676,8 @@ You can use any assertion library instead of or in addition to the built-in one, | |||
|
|||
This won't give you as nice an experience as you'd get with the [built-in assertions](#assertions) though, and you won't be able to use the [assertion planning](#assertion-planning) ([see #25](https://github.com/avajs/ava/issues/25)). | |||
|
|||
You'll have to configure AVA to not fail tests if no assertions are executed, because AVA can't tell if custom assertions pass. Set the `failWithoutAssertions` option to `false` in AVA's [`package.json` configuration](#configuration). |
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 feature, while useful, is going to hit a lot of people. Any chance we could add support for assertions throwing AssertionError
?
I know we have #1094, but that's more about integrating with t
and t.plan()
. I sometimes use assertions helper, and would like to use them in AVA without having to do anything extra.
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 feature, while useful, is going to hit a lot of people. Any chance we could add support for assertions throwing AssertionError?
That's not the problem, actually. Any custom assertion that throws an error causes the test to fail, just because tests shouldn't throw errors.
The issue is when all custom assertions pass and no errors are thrown. The intent is for the test to pass, but we don't know that custom assertions were executed. So it'll fail. That's why I added this option.
IMHO the question is what the default behavior should be.
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.
Could we maybe have it on by default, but disable it if the user imports any of the popular assertion libs? That way we can have a good default, but also support people using external assertion libs without them having to configure something.
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'm not sure how we can make that detection reliable and consistent across test files.
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.
Me neither. Maybe not worth it. We can discuss in a new issue after this is merged. Let's go with on by default for now.
this.finishDueToInactivity = () => { | ||
const err = returnedObservable ? | ||
new Error('Observable returned by test never completed') : | ||
new Error('Promise returned by test never resolved'); |
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 commit is an amazing improvement! I remember trying to achieve something like this in AVA 0.0.2, but I could not get it working. Probably because beforeExit
didn't exist then.
Significantly more readable, indeed. |
Mark, you're on 🔥! |
OK so with import test from 'ava'
test('sync', t => {
const err = t.throws(() => {})
console.error('After sync', err)
}) This results in:
Now async: test('async', async t => {
const err = await t.throws(Promise.resolve())
console.error('After async', err)
})
What happens is that the promise returned by With this branch:
(OK just now noticing that the output for Now, both |
Pushed a commit to fix this. |
Got it. My problem with this change is that it breaks a popular pattern that we also document: test('async', async t => {
const err = await t.throws(Promise.resolve());
t.is(err.message, 'foo');
}) Will result in:
Which is not very helpful for the user. Do you remember why and when we stopped throwing on assertion failures? Seems like that would have resolved all of this, as no code would run after a failed assertion. What is even the benefit of assertions not throwing? Another workaround would be to just return an empty object until we implement #1047. I just don't want to force the user to change their code twice. |
Interesting! That's happening because we now only check the pending assertions for failures after we've checked the return value. I think that's a bug, even if
I don't know. It's hard to tell, currently
If you have a callback test (or even when you wrap a legacy API in a promise), code may run in a separate call stack. If the assertion throws that might become an uncaught exception, completely crashing your test run.
That's an option, though with the bug I described above it would mean the (Unless of course they cause an uncaught exception in some odd test, but even returning an empty object could cause that.) |
I've pushed a fix:
|
@novemberborn Amazing. I'm good with it now. |
No comments on this PR yet, just some background: As far as I know, we have never allowed assertions failures to throw. This is a relic from In test('add', t => {
t.is(add(2, 2), 4);
t.is(add(2, 3), 5);
t.is(add(3, 3), 6);
// ....
}); In AVA, you would stop at the first failure, so it's better to use macros: function macro(t, lhs, rhs, expected) {
t.is(add(lhs, rhs), expected);
}
test(macro, 2, 2, 4);
test(macro, 2, 3, 5);
test(macro, 3, 3, 6); This moves each assertion into separate tests, so a failure of one does not mask failures down the line.
Right now, what we are doing does not make a lot of sense. We only display the first failed assertion, but we continue on after that assertion collecting data we will never display. We have discussed a flag to display every failed assertion in #261. |
const path = require('path'); | ||
const chalk = require('chalk'); | ||
|
||
const isForked = typeof process.send === '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.
I am pretty sure moving this in here will break karma-ava
. Anything that relies on the Node environment (vs the browser) should remain in process-adapter
.
process-adapter
is swapped out by karma-ava
when compiling for the browser with an implementation that polyfills any missing functionality.
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.
karma-ava
is pretty experimental though. We don't have a stable API so IMHO it's fine if this breaks karma-ava
.
If necessary this could live in its own file. It's odd in process-adapter
. Heck it's a bit odd to have it in test-worker
versus just main
.
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 am not so concerned about breaking karma-ava
in the short term. But we need a strategy for where we will locate Node stuff that needs to be polyfilled for the browser. This is going to necessarily create some "oddness", as you will encounter the need to abstract out stuff you might prefer to inline throughout the codebase.
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, let's cross that bridge when we get there.
test/fixture/long-running.js
Outdated
@@ -2,6 +2,7 @@ import test from '../../'; | |||
|
|||
test.cb('slow', t => { | |||
setTimeout(t.end, 5000); | |||
t.pass(); |
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.
We shouldn't force users to use t.pass
in this scenario. You can pass t.end
as an error first callback, so passing t.end
really should count as performing an assertion.
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. Will push a fix imminently.
return this._results(); | ||
let activeRunnable; | ||
const onBeforeExit = () => { | ||
if (activeRunnable.finishDueToInactivity) { |
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.
Where is the finishDueToInactivity
method defined?
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 Test
. See 3e3b213.
Rather than keeping an infinite timer open, waiting for `t.end()` to be called, fail the callback test if it is not ended when the event loop empties. Similarly, fail promise/observable returning tests if the promise hasn't fulfilled or the observable hasn't completed when the event loop empties. Note that user code can keep the event loop busy, e.g. if it's listening on a socket or starts long timers. Even after this change, async tests may hang if the event loop is kept busy.
If the assertion fails, the AssertionError no longer has access to the stack of when the assertion was called. Record it before entering the promise chain.
There are too many parameters.
If a pending assertion fails before the test implementation returns a rejected promise, the assertion failure should cause the test to fail, not the promise rejection.
a0bbb19
to
950e71d
Compare
Another massive PR… the first bunch of commits are cleanups and refactorings that lay groundwork for the subsequent work.
Highlights:
t.throws()
andt.notThrows()
, when passed a promise or observable, no longer reject the returned promise with theAsssertionError
. Instead the promise is fulfilled withundefined
. This behavior is consistent with when a function is passed, and with other assertions which also do not throw when they fail.Concurrent
,Sequence
andTest
have been refactored and are (hopefully) easier to understand.Similarly
test-worker
andprocess-adapter
are slightly easier to understand.It's now a failure to add an assertion after a test finishes. That said, the failure is only shown if you used
t.throws()
ort.notThrows()
assertions with a promise or observable. I'll open an issue when this PR lands discussing how to make the failure count in other scenarios.It's now a failure for a test to finish without executing any assertions. A
failWithoutAssertions
option has been added to thepackage.json
configuration. It defaults totrue
, users should set this tofalse
to disable the behavior. This will break deployments with where custom assertions are used, or tests that don't use AVA's assertion library for other reasons. Uset.pass()
to ensure at least 1 assertion is executed.We can now detect when
t.end()
is never called in atest.cb()
test, or if a returned promise never resolves, or a returned observable never completes. This means tests will no longer hang. This does not work if user code keeps the event loop busy by starting or connecting to a server or running long timers.This also prepares for integrating avajs/babel-plugin-throws-helper#8.