Kerrick Long’s

Web Log & Articles

Demystifying Ember: Asynchronous Side-Effects in Testing

If you've just started writing acceptance tests for your Ember.js app, you may run into the following arcane-sounding error:

Assertion Failed: You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in a run

This sounds pretty confusing at first, and doesn't sound super specific about what needs to be done. When I first saw this error, I thought I needed to change something in my tests, and started wrapping random assertions and Ember Test Helpers in Ember.run(). However, the problem actually exists in your application code, even if it seems to work in production or during manual testing.

This is a fairly simple problem to fix, once you fully understand it. First, let's break down what it means on a phrase-by-phrase basis.

Side-Effects

Before we get into the internals of Ember.js and talk about the run loop, let's discuss a very fundamental aspect of programming: pure functions vs. impure functions.

A pure function is defined by two traits. First, it will always evaluate to the same return value given the same arguments, no matter how many times you call it. Second, and most importantly, it does not change any external state, modify any other objects, or otherwise affect the state of your application (it does not have side-effects). Computed properties and handlebars helpers are common examples of pure functions in an Ember.js app.

export default Ember.Component.extend({  
  // width is pure
  @computed('height')
  width: height => height * 4 / 3
});
export default Ember.Helper.extend({  
  // compute is pure
  compute: ([cents], { currency }) => `${currency}${cents * 0.01}`
});

An impure function is any function which is not pure. In an Ember.js application, your actions are usually impure functions intended to modify state based on user action.

export default Ember.Component.extend({  
  clicked: false,
  actions: {
    // buttonClicked is impure
    buttonClicked() {
      this.toggleProperty('clicked'); // side effect
    }
  }
});

Asynchronous Side-Effects

Sometimes, your impure functions will have side effects after some sort of asynchronous action like a network call. In the previous example, the clicked property was modified synchronously when buttonClicked was called. However, imagine a login form being submitted:

export default Ember.Route.extend({  
  session: Ember.inject.service(),
  actions: {
    // loginSubmitted is impure
    async loginSubmitted({ username, password }) {
      try {
        await this.get('session').authenticate(username, password); // side effect
      } catch ({ errors }) {
        this.set('controller.errors', errors); // async side effect
      }
    }
  }
});

In this case, the side-effect (modifying controller.errors) happens asynchronously, because the authenticate method that returns a rejected promise was awaited. It might happen on the next tick, 100 milliseconds later, or on a poor connection a number of seconds later. We'll circle back to this example later, so keep this in mind.

You have turned on testing mode

If you haven't been writing automated tests for your Ember.js app, you've probably never seen this error. The problem you've just discovered is that Ember.js behaves slightly differently when running automated tests than it does in development and production. This is because in a normal browser environment, everything is based on asynchronous behavior: user input. But in an automated test, your test code is driving app behavior instead.

The run-loop

If you're not familiar with the Ember.js run loop, I highly recommend checking out the interactive explainer in the Ember Guides. In brief, everything you do in an Ember app is made more efficient by the existence of the run loop.

Asynchronous

When you write synchronous JavaScript in an Ember app, it is executed by Ember.js within the context of the current run loop, so everything happens when Ember expects it. But when you write asynchronous code, you run the risk of "escaping" the current run loop. Asynchronous code can come from using callbacks, promises, or async/await.

Which disabled the run-loop's autorun

In development and production, Ember.js tries to mitigate this problem for you by "autorunning" a run loop when it detects that it needs to, to make it easier to start using Ember without thinking about the run loop. It's great for onboarding new Ember developers! However, this behavior isn't perfect, so it can cause subtle performance problems in production. In order to help you avoid these bugs (and for a few other reasons discussed here), autorun is disabled during testing.

You will need to wrap any code with asynchronous side-effects in a run

So now that you understand the run loop and its autorun, you understand what side effects are, and you are aware of how asynchronicity and the run loop interact, it's time to do exactly what Ember is telling you to do! This will fix your test failure and mitigate potential performance problems in your production application.

Going back to the previous example of our login form, here's how you'd fix this problem:

export default Ember.Route.extend({  
  session: Ember.inject.service(),
  actions: {
    // loginSubmitted is impure
    async loginSubmitted({ username, password }) {
      try {
        await this.get('session').authenticate(username, password); // side effect
      } catch ({ errors }) {
        Ember.run(() => this.set('controller.errors', errors)); // async side effect
      }
    }
  }
});

🎉 Now the line that has side effects (setting controller.errors) asynchronously (because of await) is wrapped in an Ember.run! You're no longer relying on Ember.js sniffing out the need to start a run loop once your authenticate promise is rejected, because you're telling it to do so explicitly with Ember.run. Enjoy your passing tests!

818 words