Previous: Awaiting Events / Conditions
Next: FAQ & Fact Sheet
Ember doesn't yet have strong conventions for testing
long-term timers and polling loops, and since many of the use cases
that ember-concurrency addresses involves heavy use of timeout()
,
and often times within a (possibly infinite) loop, it can be difficult
to figure out how to test code that makes heavy use of such things
within ember-concurrency tasks.
NOTE: this is an area of active development within the Ember community, particularly amongst ember-concurrency users; in due time we will probably have more official API (possibly in the form of another addon) to help make testing time more manageable, but in the meantime, this page documents some common approaches to testing time with present-day tooling.
Consider the following (common) pattern for polling a server for changes:
@task *pollForChanges() { while(true) { yield pollServerForChanges(); yield timeout(5000); } }
The above example uses ember-concurrency tasks; to demonstrate that these issues aren't limited to ember-concurrency tasks, here is how the same logic might be written without ember-concurrency:
async pollForChanges() { if (this.isDestroyed) { return; } await pollServerForChanges(); run.later(this, 'pollForChanges', 5000); }
Both of these cases involve a "poll loop": on every iteration, do something asynchronous, then pause for some period of time, then repeat.
If, within an acceptance test, you visit()
ed the page that
causes this loop to start, your acceptance test case would "hang" and eventually
fail with a QUnit test timeout. The reason this happens is that the Ember testing
tools are aware of all timers created via Ember.run.later
(and
ember-concurrency's timeout()
helper internally uses Ember.run.later
),
and will wait for all timers to "settle" before allowing the test to proceed.
But if you have a timer within a loop, the timers will never settle, and hence
your test will hang.
The solution, one way or another, is to "break" the timer loop when in a testing environment. Here are all the ways to do that, each with their own problems / tradeoffs:
Ember.testing
checks in your code@task *pollForChanges() { while(true) { yield pollServerForChanges(); if (Ember.testing) { return; } yield timeout(5000); } }
This is sufficient when it's satisfactory to just test a single iteration of a loop, but a) it won't test that the task continues to loop, and b) it's unfortunate to have to riddle your actual code with testing logic.
Ember.run.cancelTimers
in your test caseThis is the approach used by the ember-concurrency documentation site tests; since any of the pages on this docs site might demonstrate a live ember-concurrency task with a timer loop, all of the acceptance tests automatically cancel all outstanding timers after 500ms to effectively stop all tasks wherever they're paused.
If you're testing code that just uses long timers, but not necessarily loops,
you might still run into the problem of test cases that take too long to complete,
or might hit the QUnit timeout. A common solution to this problem is to use much
smaller millisecond timer values in a testing environment. You can either do this
by checking Ember.testing
wherever you set a timer, or, more elegantly, you can
define common timer values in a config file, import the timer values
wherever you need to set a timer, and in test environments, the config
file specifies much smaller values so that the timers elapse more quickly.
The above solutions leave much to be desired. Hopefully a definitive solution
that produces clear, deterministic, consistent results will emerge from the
community. There are some ideas
floating around, and if you're interested in contributing to the discussion
please join the #e-concurrency
channel on the Ember Community Discord server.
Also, if you're finding success with a testing approach that wasn't mentioned here, please open a GitHub issue with your ideas or open a Pull Request to add additional docs to this page.
Sometimes it's not obvious why a Task was canceled; in these cases you
can use the debug
Task Modifier on a specific task e.g.
@task({ debug: true }) *nameOfTask { /* ... */ }
,
which will provide some logging about the task's lifecycle, e.g.
TaskInstance 'nameOfTask' was canceled because the object it lives on was destroyed or unrendered
.
To enable lifecycle logging on ALL ember-concurrency tasks, you can enable the DEBUG_TASKS
flag on EmberENV
in your project's config/environment.js
file.
Check the requirements to see what you need to install.
If you are sure that you fulfilled the requirements correctly, but are still
experiencing weird errors, install
ember-cli-dependency-lint
to ensure that you are not accidentally including outdated versions of
ember-concurrency
as a transitive dependency. You may need to file
an issue with the relevant dependency to update their ember-concurrency dependency
range. Alternatively, you might resort to using something like Yarn resolutions
to enforce your application's version of ember-concurrency, but this depends on
how its being used by the dependency.
If it's still not working after that, please reach out on the
#e-concurrency
channel on the
Ember Community Discord server or file
a new issue.
Previous: Awaiting Events / Conditions
Next: FAQ & Fact Sheet