Ember.js provides a robust testing framework built right in, making it easy to write and maintain tests for your applications. This comprehensive guide covers everything you need to know about testing Ember applications.
Ember Testing Architecture
Ember's testing stack includes:
- 1QUnit: Default test framework
- 2Ember Test Helpers: Specialized helpers for Ember testing
- 3Test Loader: Automatically finds and runs tests
- 4Test Server: Development server for running tests
Setting Up Tests
Ember CLI generates test files automatically when you create components, routes, and other artifacts:
# Generate a component with tests
ember generate component user-card
# This creates:
# app/components/user-card.js
# tests/integration/components/user-card-test.jsUnit Testing
Unit tests isolate individual pieces of code without Ember's framework overhead.
Testing Utilities
// tests/unit/utils/format-date-test.js
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Utility | format-date', function (hooks) {
setupTest(hooks);
test('formats date correctly', function (assert) {
const formatDate = this.owner.lookup('utility:format-date');
const date = new Date('2026-03-27');
const result = formatDate(date);
assert.equal(result, 'March 27, 2026');
});
});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('authenticates user with valid credentials', async function (assert) {
const session = this.owner.lookup('service:session');
await session.authenticate('user@example.com', 'password');
assert.true(session.isAuthenticated);
});
test('rejects invalid credentials', async function (assert) {
const session = this.owner.lookup('service:session');
try {
await session.authenticate('user@example.com', 'wrong');
assert.ok(false, 'Should have thrown error');
} catch (error) {
assert.equal(error.message, 'Invalid credentials');
}
});
});Integration Testing
Integration tests verify that components work correctly within Ember's runtime.
Component Integration Tests
// tests/integration/components/user-card-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | user-card', function (hooks) {
setupRenderingTest(hooks);
test('renders user information', async function (assert) {
this.set('user', {
name: 'John Doe',
email: 'john@example.com',
});
await render(hbs`<UserCard @user={{this.user}} />`);
assert.dom('[data-test-user-name]').hasText('John Doe');
assert.dom('[data-test-user-email]').hasText('john@example.com');
});
test('handles missing user', async function (assert) {
await render(hbs`<UserCard />`);
assert.dom('[data-test-no-user]').exists();
});
});Testing Component Actions
test('triggers action on button click', async function (assert) {
assert.expect(1);
this.set('action', (value) => {
assert.equal(value, 'clicked');
});
await render(hbs`
<MyButton @onClick={{this.action}} />
`);
await click('[data-test-button]');
});Testing Component State
test('updates state on input', async function (assert) {
this.set('value', 'initial');
await render(hbs`
<MyInput @value={{this.value}} />
`);
assert.dom('input').hasValue('initial');
await fillIn('input', 'updated');
assert.dom('input').hasValue('updated');
});Acceptance Testing
Acceptance tests simulate user interactions with your application.
Basic Acceptance Test
// tests/acceptance/login-test.js
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit, fillIn, click } from '@ember/test-helpers';
module('Acceptance | login', function (hooks) {
setupApplicationTest(hooks);
test('user can login', async function (assert) {
await visit('/login');
await fillIn('[data-test-email-input]', 'user@example.com');
await fillIn('[data-test-password-input]', 'password');
await click('[data-test-login-button]');
assert.dom('[data-test-user-greeting]').exists();
assert.equal(currentURL(), '/dashboard');
});
});Testing Navigation
test('navigates to user profile', async function (assert) {
await visit('/');
await click('[data-test-profile-link]');
assert.equal(currentURL(), '/profile');
assert.dom('[data-test-profile-page]').exists();
});Testing Form Submission
test('submits contact form', async function (assert) {
const server = pretenderServer();
server.post('/api/contact', (request) => {
return [200, {}, ''];
});
await visit('/contact');
await fillIn('[data-test-name]', 'John Doe');
await fillIn('[data-test-email]', 'john@example.com');
await fillIn('[data-test-message]', 'Test message');
await click('[data-test-submit-button]');
assert.dom('[data-test-success-message]').exists();
});Test Helpers
Ember provides powerful test helpers for common interactions.
DOM Helpers
import {
find,
findAll,
triggerEvent,
} from '@ember/test-helpers';
test('DOM manipulation', async function (assert) {
await render(hbs`<div class="container">Content</div>`);
const element = find('.container');
assert.ok(element);
const allElements = findAll('.container');
assert.equal(allElements.length, 1);
await triggerEvent('.container', 'mouseenter');
});Routing Helpers
import {
currentURL,
visit,
click,
} from '@ember/test-helpers';
test('routing', async function (assert) {
await visit('/');
assert.equal(currentURL(), '/');
await click('[data-test-about-link]');
assert.equal(currentURL(), '/about');
});Wait Helpers
import {
waitFor,
waitUntil,
} from '@ember/test-helpers';
test('async operations', async function (assert) {
await render(hbs`<AsyncComponent />`);
await waitFor('[data-test-loaded-content]');
assert.dom('[data-test-loaded-content]').exists();
});Testing with Mirage
Mirage provides a mock server for API testing.
Setup Mirage
// mirage/config.js
export function makeServer(config) {
const finalConfig = {
...config,
routes() {
this.namespace = 'api';
this.get('/users', (schema) => {
return schema.users.all();
});
this.post('/users', (schema, request) => {
const attrs = JSON.parse(request.requestBody);
return schema.users.create(attrs);
});
},
};
return new Server(finalConfig);
}Test with Mirage
test('loads users from API', async function (assert) {
server.createList('user', 3);
await visit('/users');
assert.dom('[data-test-user]').exists({ count: 3 });
});Testing Async Code
Testing Promises
test('handles promise resolution', async function (assert) {
const promise = Promise.resolve('success');
const result = await promise;
assert.equal(result, 'success');
});Testing Ember Concurrency Tasks
import { task } from 'ember-concurrency';
test('performs async task', async function (assert) {
const instance = this.owner.lookup('service:my-service');
const task = instance.myTask;
task.perform();
await waitUntil(() => task.state === 'idle');
assert.true(instance.taskCompleted);
});Testing Edge Cases
Error Handling
test('handles API errors gracefully', async function (assert) {
const server = pretenderServer();
server.get('/api/data', () => {
return [500, {}, 'Internal Server Error'];
});
await visit('/data');
assert.dom('[data-test-error-message]').exists();
assert.dom('[data-test-error-message]').hasText('Failed to load data');
});Loading States
test('shows loading state', async function (assert) {
const server = pretenderServer();
server.get('/api/data', () => {
return [200, {}, '[]'];
}, 1000); // 1 second delay
visit('/data');
assert.dom('[data-test-loading]').exists();
await waitFor('[data-test-content]');
assert.dom('[data-test-loading]').doesNotExist();
});Testing Best Practices
1. Use Test Selectors
{{! Good: Test-specific attributes }}
<button data-test-submit-button>Submit</button>
{{! Bad: CSS classes }}
<button class="btn btn-primary">Submit</button>2. Test Behavior, Not Implementation
// Good: Tests user interaction
test('user can submit form', async function (assert) {
await fillIn('[data-test-input]', 'value');
await click('[data-test-submit]');
assert.dom('[data-test-success]').exists();
});
// Bad: Tests implementation details
test('form calls submit method', async function (assert) {
const component = this.owner.lookup('component:my-form');
assert.equal(component.submit.called, true);
});3. Keep Tests Independent
// Each test should be able to run independently
test('test 1', async function (assert) {
// Don't depend on state from test 2
});
test('test 2', async function (assert) {
// Don't depend on state from test 1
});4. Use Page Objects
// tests/helpers/page-object.js
export const loginPage = {
emailInput: '[data-test-email-input]',
passwordInput: '[data-test-password-input]',
submitButton: '[data-test-login-button]',
async fill(email, password) {
await fillIn(this.emailInput, email);
await fillIn(this.passwordInput, password);
},
async submit() {
await click(this.submitButton);
},
};
// Usage
test('logs in user', async function (assert) {
await loginPage.fill('user@example.com', 'password');
await loginPage.submit();
assert.dom('[data-test-dashboard]').exists();
});Continuous Integration
GitHub Actions
# .github/workflows/test.yml
name: Test Suite
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Generate coverage
run: npm test:coverageCode Coverage
Generate coverage reports:
# Install coverage tool
ember install ember-cli-code-coverage
# Run tests with coverage
ember test --coverageConclusion
Testing Ember.js applications is straightforward with Ember's built-in testing framework. By writing comprehensive tests at the unit, integration, and acceptance levels, you can ensure your application is reliable and maintainable.
Remember these key principles:
- 1Test user behavior, not implementation
- 2Keep tests independent and fast
- 3Use test selectors for element selection
- 4Mock external dependencies
- 5Test edge cases and error conditions
- 6Maintain high test coverage
- 7Run tests in CI/CD pipeline
With proper testing practices, you'll catch bugs early, ship with confidence, and maintain high code quality throughout your Ember application's lifecycle.