Testing

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.

The Problem

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:

Insert 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.

Use Ember.run.cancelTimers in your test case

This 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.

No loops, but long timers

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 Future

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.

Debugging

Unexpected Cancelation

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.

Strange errors when using as documented

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.