Testing Async in Ember.js — Part One

Chris Masters
5 min readAug 12, 2016

--

The testing story for Ember is really great, but from time you time you may run into a situation that veers a little too far from the beaten track to be covered by the usual methods.

That means you’ll have to dig a little deeper to get your tests passing but, depending on the scenario (and type of test), there are a few approaches that are useful to know.

Using registerWaiter to wait for async code to finish

This technique works for both integration and acceptance tests, which makes it quite versatile.

It’s useful when the async behaviour is outside of the usual Ember run loop code, and so therefore the use of andThen (for acceptance), or wait() (for integration) alone is not sufficient.

A good example of this is some behaviour triggered by an image loading event handler.

At it’s most simple the code could be something like this:

const img = new Image();
img.onload = event => {
triggerSomeAction();
}
img.src = 'http://image.url/path.jpg';

The Run Loop

The first thing to note about this code is that the callback is not currently included in the Run Loop.

The Ember guides have a good explanation (worth reading in full) of why this is necessary, and the impact when testing.

… Some of Ember’s test helpers are promises that wait for the run loop to empty before resolving. If your application has code that runs outside a run loop, these will resolve too early and give erroneous test failures which are difficult to find…

So in our example the resulting code would be:

const img = new Image();
img.onload = event => {
Ember.run(() => {
triggerSomeAction();
});
}
img.src = 'http://image.url/path.jpg';

Promises

The next step to taming the asynchronous code is following Ember’s approach by using promises. Again the guides provide a good explanation and is worth reading in full.

In short, promises are objects that represent an eventual value. A promise can either fulfill(successfully resolve the value) or reject (fail to resolve the value). The way to retrieve this eventual value, or handle the cases when the promise rejects, is via the promise’s then() method, which accepts two optional callbacks, one for fulfillment and one for rejection.

We can wrap this code in a promise like this:

let promise = new Ember.RSVP.Promise((resolve, reject) => {
const img = new Image();
img.onload = event => {
Ember.run(() => {
resolve(img);
});
};
img.onerror = () => {
Ember.run(() => reject());
};
img.src = 'http://image.url/path.jpg';
});

So that now the loading and error behaviour is encapsulated in a promise.

Integration tests

In the case of an integration test you can pause the test to wait for all asynchronous behaviour to have finished by using the wait() test helper. From the guides:

Often, interacting with a component will cause asynchronous behavior to occur, such as HTTP requests, or timers. The wait helper is designed to handle these scenarios, by providing a hook to ensure assertions are made after all Ajax requests and timers are complete.

At it’s most basic a test would look like this:

import { moduleForComponent, test } from 'ember-qunit';
import wait from 'ember-test-helpers/wait';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('my component', 'working', {
integration: true
});
test('works', function(assert) {

this.render(hbs`{{my-component src='http://emberjs.com/images/tomsters/sandiego-zoey.png'}}`);

return wait().then(() => {
assert.equal(someIndicator, 'image loaded');
});

Acceptance tests

For now (until Robert Jackson’s unified testing story lands) to do this in acceptance tests is slightly different and uses andThen(). From the guides on this wait helper.

The andThen helper will wait for all preceding asynchronous helpers to complete prior to progressing forward. Let’s take a look at the following example.

At it’s most basic a test would look like this:

import { test } from 'qunit';
import moduleForAcceptance from '../../tests/helpers/module-for-acceptance';
moduleForAcceptance('image loaded');test('/', function(assert) {
visit('/');
andThen(function() {
assert.equal(someIndicator, 'image loaded');
});
});

registerWaiter

Unfortunately, for this particular use case completing those steps is not quite enough. The reason is that not all promises or run loops are included by default when determining what makes the test helpers wait. In general AJAX requests and timers should automatically work, but other scenarios are not so clear.

So to make explicit we want the tests to wait for our images to load we need to register them ourselves.

The guides are more limited on this, but the API documents explain

This allows ember-testing to play nicely with other asynchronous events, such as an application that is waiting for a CSS3 transition or an IndexDB transaction.

In effect, once a waiter is registered the test waits and polls the method until it returns true.

One thing I don’t like so much about this is it means that you must include test-only code in your application code. In reality this isn’t so bad as you can wrap the code in a conditional Ember.testing pretty easily but it is something which hopefully will be possible by other means soon.

So in this example it can be achieved by using init() to set a variable as false and registering the waiter like this:

init() {
this._super(...arguments);
if (Ember.Testing) {
this._loading = false;
Ember.Test.registerWaiter(() => this._loading === false);
}
}

And then setting the variable to true whilst the image is loading and using promise.finally() to set it back to false once the image has loaded like this:

if (Ember.testing) {
this._loading = true;
return promise.finally(() => this._loading = false);
}

return promise;

After this both the acceptance test and the integration test will know to wait until the image(s) have loaded.

Ember Twiddle examples

Thanks to Ember Twiddle it’s possible to run QUnit tests on Ember code in the browser, so I’ve created a couple of real world examples based on this use case so you can run the tests and make changes to see it working for yourself.

Conclusion

This is a very flexible way of incorporating otherwise hard to test code into your tests. The main downside is that it introduces some test-specific code into your application code base, but in return it makes both acceptance and integration testing possible without any additional work.

This is particularly useful for integration tests which don’t currently have async custom test helpers.

Thanks for reading and if you have any feedback I’d love to hear from you—part of my motivation for writing this is to find out whether other people use similar approaches, or have better alternatives.

--

--