Skip to content

Ember Testing — Test-Driven Development in Ember

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Ember Testing. We cover key concepts, practical examples, and best practices to help you master this topic.

Ember testing uses QUnit by default, with ember-cli-mirage for mocking server data and @ember/test-helpers for DOM interaction. Tests are organized into unit (models, services, helpers), integration (components), and acceptance (full page flows).

What You'll Learn

You will learn how to write unit tests for models and services, integration tests for components, acceptance tests for routes, and mock server data with Mirage.

Why It Matters

Ember's CLI generates test files automatically. The testing infrastructure is built-in. Writing tests from the start prevents regressions and makes Refactoring safe.

Real-World Use

A financial dashboard with 100+ components maintains 95% test coverage. Every route, component, model, and service has tests. Mirage simulates the trading API for acceptance tests. CI runs the full suite in under 3 minutes.

flowchart LR
    A[Test Pyramid] --> B[Acceptance Tests]
    A --> C[Integration Tests]
    A --> D[Unit Tests]
    B --> E[Mirage + async helpers]
    C --> F[Component rendering + interaction]
    D --> G[Pure logic validation]

Running Tests

ember test            # Run once in CI mode
ember test --server   # Watch mode with browser
ember test --filter="model"  # Run specific tests

Unit Testing Models

// tests/unit/models/product-test.js
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Model | product', function(hooks) {
  setupTest(hooks);

  test('it has correct default values', function(assert) {
    let store = this.owner.lookup('service:store');
    let product = store.createRecord('product', {
      name: 'Widget',
      price: 29.99
    });

    assert.equal(product.name, 'Widget');
    assert.equal(product.price, 29.99);
    assert.notOk(product.inStock);
  });

  test('it computes formatted price', function(assert) {
    let store = this.owner.lookup('service:store');
    let product = store.createRecord('product', {
      name: 'Widget',
      price: 29.99
    });

    assert.equal(product.formattedPrice, '$29.99');
  });

  test('it validates required fields', async function(assert) {
    let store = this.owner.lookup('service:store');
    let product = store.createRecord('product', { name: '' });

    try {
      await product.save();
      assert.ok(false, 'Should have thrown');
    } catch (error) {
      assert.ok(error, 'Validation failed');
    }
  });
});

Unit Testing Services

// tests/unit/services/session-test.js
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Service | session', function(hooks) {
  setupTest(hooks);

  test('it starts unauthenticated', function(assert) {
    let session = this.owner.lookup('service:session');
    assert.notOk(session.isAuthenticated);
    assert.equal(session.user, null);
  });

  test('it handles login', async function(assert) {
    let session = this.owner.lookup('service:session');

    // Mock the fetch call
    let originalFetch = window.fetch;
    window.fetch = () => Promise.resolve({
      json: () => Promise.resolve({
        user: { id: 1, name: 'Alice', role: 'admin' },
        token: 'abc123'
      })
    });

    let result = await session.login('alice@test.com', 'password');
    assert.ok(result.success);
    assert.ok(session.isAuthenticated);
    assert.equal(session.user.name, 'Alice');
    assert.ok(session.isAdmin);

    window.fetch = originalFetch;
  });

  test('it clears state on logout', function(assert) {
    let session = this.owner.lookup('service:session');
    session.user = { name: 'Alice' };
    session.isAuthenticated = true;

    session.logout();

    assert.notOk(session.isAuthenticated);
    assert.equal(session.user, null);
  });
});

Integration Testing Components

// tests/integration/components/login-form-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, fillIn, click } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';

module('Integration | Component | login-form', function(hooks) {
  setupRenderingTest(hooks);

  test('it renders login form', async function(assert) {
    await render(hbs`<LoginForm />`);
    assert.dom('input[type="email"]').exists();
    assert.dom('input[type="password"]').exists();
    assert.dom('button[type="submit"]').hasText('Login');
  });

  test('it shows error on empty submit', async function(assert) {
    await render(hbs`<LoginForm />`);
    await click('button[type="submit"]');
    assert.dom('.error').exists();
  });

  test('it calls onSubmit with credentials', async function(assert) {
    this.set('handleSubmit', (data) => {
      assert.equal(data.email, 'test@test.com');
      assert.equal(data.password, 'secret');
    });

    await render(hbs`<LoginForm @onSubmit={{this.handleSubmit}} />`);

    await fillIn('input[type="email"]', 'test@test.com');
    await fillIn('input[type="password"]', 'secret');
    await click('button[type="submit"]');
  });

  test('it shows submitting state', async function(assert) {
    // Return a promise that does not resolve
    this.set('handleSubmit', () => new Promise(() => {}));

    await render(hbs`<LoginForm @onSubmit={{this.handleSubmit}} />`);

    await fillIn('input[type="email"]', 'test@test.com');
    await fillIn('input[type="password"]', 'secret123');
    await click('button[type="submit"]');

    assert.dom('button[type="submit"]').hasText('Logging in...');
    assert.dom('button[type="submit"]').isDisabled();
  });
});

Acceptance Testing with Mirage

// tests/acceptance/product-list-test.js
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit, click, currentURL, fillIn } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage';

module('Acceptance | product list', function(hooks) {
  setupApplicationTest(hooks);
  setupMirage(hooks);

  test('user can view products', async function(assert) {
    // Set up mock data
    this.server.createList('product', 3);

    await visit('/products');
    assert.equal(currentURL(), '/products');
    assert.dom('[data-test-product-card]').exists({ count: 3 });
  });

  test('user can filter products by category', async function(assert) {
    this.server.create('product', { name: 'Laptop', category: 'electronics' });
    this.server.create('product', { name: 'Shirt', category: 'clothing' });

    await visit('/products');
    await click('[data-test-category="electronics"]');

    assert.dom('[data-test-product-card]').exists({ count: 1 });
    assert.dom('[data-test-product-name]').hasText('Laptop');
  });

  test('user can search products', async function(assert) {
    this.server.create('product', { name: 'Wireless Mouse' });
    this.server.create('product', { name: 'Keyboard' });

    await visit('/products');
    await fillIn('[data-test-search]', 'mouse');

    assert.dom('[data-test-product-card]').exists({ count: 1 });
    assert.dom('[data-test-product-name]').hasText('Wireless Mouse');
  });
});

Test Helpers

// Custom test helpers
import { click, triggerKeyEvent } from '@ember/test-helpers';

// Simulate complex interactions
async function selectFromAutocomplete(inputSelector, searchText, optionText) {
  await fillIn(inputSelector, searchText);
  await triggerKeyEvent(inputSelector, 'keydown', 'ArrowDown');
  await triggerKeyEvent(inputSelector, 'keydown', 'Enter');
}

// Usage in tests
test('user can search and select', async function(assert) {
  await render(hbs`<Autocomplete @options={{this.items}} />`);
  await selectFromAutocomplete('input', 'ali', 'Alice');
  assert.dom('[data-test-selected]').hasText('Alice');
});

Testing Query Parameters

// tests/acceptance/product-search-test.js
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit, currentURL, fillIn, click } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage';

module('Acceptance | product search', function(hooks) {
  setupApplicationTest(hooks);
  setupMirage(hooks);

  test('query parameters sync with URL', async function(assert) {
    this.server.createList('product', 5);

    await visit('/products');
    await fillIn('[data-test-search]', 'laptop');

    assert.ok(currentURL().includes('query=laptop'));
  });

  test('URL query parameters restore state', async function(assert) {
    this.server.create('product', { name: 'Laptop Pro', category: 'electronics' });
    this.server.create('product', { name: 'Laptop Air', category: 'electronics' });

    await visit('/products?category=electronics');

    assert.dom('[data-test-product-card]').exists({ count: 2 });
  });
});

Common Mistakes

  1. Not resetting Mirage between tests. Mirage state carries over. Use setupMirage(hooks) to auto-reset.
  2. Testing implementation details instead of behavior. Test what the user sees and does, not internal method calls.
  3. Forgetting await on test helpers. click, fillIn, and render return promises. Without await, assertions run before rendering completes.
  4. Not using assert.dom() for DOM assertions. Manual DOM queries are fragile. assert.dom() provides clear failure messages.
  5. Creating too many acceptance tests. Acceptance tests are slow. Test critical user flows with acceptance and everything else with integration/unit tests.

Practice Questions

  1. What are the three types of tests in Ember?
  2. How does ember-cli-mirage help with acceptance testing?
  3. What is the difference between unit and integration tests?
  4. How do you test query parameter behavior?
  5. Challenge: Write a complete test suite for a ShoppingCart component: (1) Unit test the cart service (add item, remove item, calculate total), (2) Integration test the cart display component (renders items, shows empty state), (3) Acceptance test the add-to-cart flow (visit product page, click add, verify cart count).

FAQ

What testing framework does Ember use?

QUnit by default. Ember CLI also supports Mocha.

What is ember-cli-mirage?

A library for mocking server data in acceptance tests.

How do I run tests in CI?

Use ember test — it runs headlessly in CI environments.

Can I test Ember components in isolation?

Yes. Integration tests render a single component with mock data.

How do I test async operations?

Use await with test helpers. Ember's test system waits for all async operations to settle.

Mini Project

Write a complete test suite for a todo application: (1) Unit tests for the Todo model (validation, toggle). (2) Unit tests for the TodoList service (add, remove, complete, filter). (3) Integration tests for the TodoItem component (render, toggle, delete). (4) Integration tests for the TodoForm component (add new todo, validation). (5) Acceptance tests for the full todo flow (add todos, mark complete, filter active/completed, clear completed).

What's Next

Now that you understand testing, learn Ember Octane for modern Ember features. Then build a complete project in Ember Project.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro