Ember Testing — Test-Driven Development in Ember
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
- Not resetting Mirage between tests. Mirage state carries over. Use
setupMirage(hooks)to auto-reset. - Testing implementation details instead of behavior. Test what the user sees and does, not internal method calls.
- Forgetting
awaiton test helpers.click,fillIn, andrenderreturn promises. Without await, assertions run before rendering completes. - Not using
assert.dom()for DOM assertions. Manual DOM queries are fragile.assert.dom()provides clear failure messages. - Creating too many acceptance tests. Acceptance tests are slow. Test critical user flows with acceptance and everything else with integration/unit tests.
Practice Questions
- What are the three types of tests in Ember?
- How does ember-cli-mirage help with acceptance testing?
- What is the difference between unit and integration tests?
- How do you test query parameter behavior?
- Challenge: Write a complete test suite for a
ShoppingCartcomponent: (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
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