Ember.js
Async calls
Testing
JavaScript
Frontend development

Async call in ember testing

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Asynchronous behavior is normal in Ember applications. Routes load data, components trigger tasks, and services talk to APIs. Tests only stay reliable if they wait for those operations in a way that matches Ember’s run loop and rendering lifecycle.

Modern Ember testing already does a lot of waiting for you. The important part is knowing when built-in helpers are enough and when custom async work needs an explicit waiter.

Let Ember Test Helpers Do the Waiting

The first rule is simple: if you are using official test helpers, await them. Helpers such as visit, click, fillIn, and render return promises that resolve after Ember reaches a settled state.

A rendering test might look like this:

javascript
1import { module, test } from "qunit";
2import { setupRenderingTest } from "ember-qunit";
3import { render, click, settled } from "@ember/test-helpers";
4import { hbs } from "ember-cli-htmlbars";
5
6module("Integration | Component | save-button", function (hooks) {
7  setupRenderingTest(hooks);
8
9  test("it shows success after an async save", async function (assert) {
10    this.set("saveRecord", async () => {
11      await new Promise((resolve) => setTimeout(resolve, 10));
12      this.set("didSave", true);
13    });
14
15    await render(hbs`<SaveButton @onSave={{this.saveRecord}} />`);
16    await click("[data-test-save]");
17    await settled();
18
19    assert.dom("[data-test-status]").hasText("Saved");
20  });
21});

The helper calls plus await settled() give Ember time to process promise callbacks, rerender the template, and finish queued work.

Testing Data Loading in Application Tests

Application tests often involve routing and network requests. In those tests, the cleanest solution is to stub the backend and then await navigation.

javascript
1import { module, test } from "qunit";
2import { setupApplicationTest } from "ember-qunit";
3import { visit, settled } from "@ember/test-helpers";
4
5module("Acceptance | widgets", function (hooks) {
6  setupApplicationTest(hooks);
7
8  test("it renders widgets from the API", async function (assert) {
9    this.server.get("/api/widgets", () => {
10      return {
11        widgets: [{ id: "1", name: "Primary Widget" }],
12      };
13    });
14
15    await visit("/widgets");
16    await settled();
17
18    assert.dom("[data-test-widget-name]").hasText("Primary Widget");
19  });
20});

If your project uses Mirage or Pretender, that network layer is already integrated into the test environment, which helps keep tests deterministic.

When Built-In Settling Is Not Enough

Problems start when async work happens outside Ember’s normal awareness. Common examples include:

  • Manual fetch calls not tracked by the test environment
  • Third-party SDK callbacks
  • Timers or web socket events
  • Custom promises stored in services

In those cases, the test can finish before the async work is done, leading to flaky assertions. The fix is to register a test waiter.

javascript
1import { buildWaiter } from "@ember/test-waiters";
2
3const waiter = buildWaiter("external-request");
4
5export async function trackedAsyncOperation(callback) {
6  let token = waiter.beginAsync();
7
8  try {
9    return await callback();
10  } finally {
11    waiter.endAsync(token);
12  }
13}

You can then wrap the custom async operation so Ember knows the test should keep waiting until that promise resolves.

Example with a Service

Suppose a service fetches data with a plain async call. Wrapping the operation with a waiter makes the test environment aware of it.

javascript
1import Service from "@ember/service";
2import { trackedAsyncOperation } from "../utils/tracked-async-operation";
3
4export default class ProfileService extends Service {
5  async loadProfile() {
6    return trackedAsyncOperation(async () => {
7      let response = await fetch("/api/profile");
8      return response.json();
9    });
10  }
11}

Now your tests can keep using normal helpers and await settled() instead of inserting arbitrary timeouts.

Avoid Time-Based Tests

It is tempting to solve async failures with await new Promise((r) => setTimeout(r, 100)). That usually makes tests slower and still does not guarantee correctness. Timeouts hide the real issue, which is that the test framework does not know what work it is supposed to wait for.

A better pattern is:

  1. Stub the async dependency
  2. Trigger the UI action
  3. Await the helper or settled()
  4. Assert on the final rendered state

That gives you deterministic tests that describe behavior instead of timing guesses.

Common Pitfalls

One common mistake is forgetting to await Ember helpers. click and visit are async, so un-awaited calls can cause assertions to run before the UI updates.

Another issue is mixing real network calls into tests. Tests should control the async boundary with stubs or mock servers; otherwise failures become dependent on environment and latency.

Custom async behavior outside Ember is the biggest source of flakiness. If settled() does not seem to wait, that usually means the work is not registered with Ember’s waiter system.

Finally, avoid asserting on transient loading states unless your test deliberately waits for that specific intermediate moment. Otherwise the test may race past it.

Summary

  • In Ember tests, always await official test helpers such as visit, render, and click.
  • 'settled() is useful when you need to wait for follow-up async work and rerenders.'
  • Stub API calls so tests stay deterministic and do not depend on real network timing.
  • Use test waiters for async operations that Ember cannot detect automatically.
  • Avoid raw timeouts; they usually hide synchronization problems instead of solving them.

Course illustration
Course illustration

All Rights Reserved.