Skip to content
All essays
WebMarch 27, 202614 min

Testing Ember.js Applications: Complete Guide to Quality Assurance

Master Ember.js testing with comprehensive guides for unit tests, integration tests, acceptance tests, and testing best practices.

Ü
Ümit Uz
Mobile & Full Stack Developer

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:

  1. 1QUnit: Default test framework
  2. 2Ember Test Helpers: Specialized helpers for Ember testing
  3. 3Test Loader: Automatically finds and runs tests
  4. 4Test Server: Development server for running tests

Setting Up Tests

Ember CLI generates test files automatically when you create components, routes, and other artifacts:

bash
# Generate a component with tests
ember generate component user-card

# This creates:
# app/components/user-card.js
# tests/integration/components/user-card-test.js

Unit Testing

Unit tests isolate individual pieces of code without Ember's framework overhead.

Testing Utilities

javascript
// 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

javascript
// 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

javascript
// 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

javascript
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

javascript
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

javascript
// 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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
// 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

javascript
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

javascript
test('handles promise resolution', async function (assert) {
  const promise = Promise.resolve('success');

  const result = await promise;
  assert.equal(result, 'success');
});

Testing Ember Concurrency Tasks

javascript
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

javascript
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

javascript
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

handlebars
{{! 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

javascript
// 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

javascript
// 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

javascript
// 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

yaml
# .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:coverage

Code Coverage

Generate coverage reports:

bash
# Install coverage tool
ember install ember-cli-code-coverage

# Run tests with coverage
ember test --coverage

Conclusion

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:

  1. 1Test user behavior, not implementation
  2. 2Keep tests independent and fast
  3. 3Use test selectors for element selection
  4. 4Mock external dependencies
  5. 5Test edge cases and error conditions
  6. 6Maintain high test coverage
  7. 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.

Enjoyed this essay? Share it.

Related essays

Next essay
Astro Performance Optimization: Building Lightning-Fast Websites