Ember 2.x Testing
The 2.x test API was overhauled in 3.x. If you're reading a test file and you see moduleFor, moduleForComponent, andThen, or global async helpers without await, you're in the 2.x dialect.
Test types and their setup helpers
| What you're testing | 2.x helper | 3.x+ replacement |
|---|---|---|
| A unit (service, util, controller class) | moduleFor('service:foo', { ... }) |
setupTest(hooks) (then this.owner.lookup('service:foo')) |
| A component (in isolation, with a render context) | moduleForComponent('user-card', { integration: true }) |
setupRenderingTest(hooks) |
| A model (Ember Data) | moduleForModel('post', ...) |
setupTest(hooks) |
| A user flow (full app boot) | moduleForAcceptance('/posts') |
setupApplicationTest(hooks) |
These helpers come from ember-qunit (and ember-cli-qunit/ember-cli-mocha in some 2.x setups).
Anatomy of a 2.x acceptance test
// tests/acceptance/posts-test.js
import { test } from 'qunit';
import moduleForAcceptance from 'my-app/tests/helpers/module-for-acceptance';
moduleForAcceptance('Acceptance | posts');
test('visiting /posts', function (assert) {
visit('/posts');
andThen(function () {
assert.equal(currentURL(), '/posts');
assert.equal(find('.post').length, 3);
});
});
Key 2.x flavors:
- Global async helpers:
visit,click,fillIn,keyEvent,triggerEvent,find,currentURL,currentRouteName. They are globally injected, not imported. andThen(fn): waits for the test to settle, then runsfn. EachandThenblock can issue more async helpers, which are themselves followed by anotherandThen. This is the 2.x equivalent ofawait.wait(): returns a promise that resolves when settled. Useful when you need a promise outsideandThen.
The post-2.x style — await visit('/') — is the same idea, but with native async/await and imports from @ember/test-helpers.
A 2.x component (integration) test
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('user-card', 'Integration | Component | user-card', {
integration: true
});
test('renders the user name', function (assert) {
this.set('user', { name: 'Ada Lovelace' });
this.render(hbs`{{user-card user=user}}`);
assert.equal(this.$('.user-card .name').text().trim(), 'Ada Lovelace');
});
Notes:
integration: trueflips the helper into rendering mode. Without it, you get a unit-style stub container with no render context.this.render(hbs\...`)` is the 2.x render call. In 3.x+, it'sawait render(hbs\...`)from@ember/test-helpers`.this.$(...)is jQuery.qunit-dom(assert.dom(...)) didn't exist yet — assertions are usuallyassert.equal(this.$('.x').text().trim(), 'expected').- Curly invocation in the template (
{{user-card user=user}}). Angle-bracket invocation worked from 3.4 onward.
A 2.x unit test
import { moduleFor, test } from 'ember-qunit';
moduleFor('service:cart', 'Unit | Service | cart', {
// dependencies you need resolved:
needs: ['service:notifications']
});
test('adds an item', function (assert) {
const service = this.subject();
service.add({ id: 1, price: 10 });
assert.equal(service.get('items.length'), 1);
});
needs: [...]: declares which dependencies the resolver should make available. In 3.x+, this dance is replaced bythis.owner.register(...)for fakes and automatic resolution of the rest.this.subject(): instantiates the thing under test. In 3.x+, you writethis.owner.lookup('service:cart').
wait() and pending work
wait() from ember-test-helpers resolves when:
- All promises tracked by the test container are resolved.
- No pending AJAX requests (via
jQuery.active). - No timers in the run loop.
If a test hangs forever in andThen, almost always it's because some async work isn't registered — usually a raw setTimeout, a fetch (jQuery's AJAX is tracked, fetch isn't), or a promise created outside the run loop. Test waiters (Ember.Test.registerWaiter(fn)) let you teach the framework to wait for arbitrary work; modern tests use @ember/test-waiters instead.
The bridge — running both APIs side-by-side
Most apps modernize one test file at a time. To make this safe:
- In 2.x apps near the end of life, install
ember-qunit@^4(which supports bothmoduleFor*andsetupTest). - Convert one file at a time: replace
moduleForXxxwith the matchingmodule(...) { setupXxx(hooks); }block. - Replace each
andThen(...)withawaiton the preceding helper. - Replace
this.$(...)withfind(...)orassert.dom(...).
Example before/after:
// 2.x style
moduleForAcceptance('Acceptance | posts');
test('visiting /posts', function (assert) {
visit('/posts');
andThen(() => assert.equal(currentURL(), '/posts'));
});
// modernized
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { currentURL } from '@ember/test-helpers';
import page from 'my-app/tests/pages/posts';
module('Acceptance | posts', function (hooks) {
setupApplicationTest(hooks);
test('visiting /posts', async function (assert) {
await page.visit();
assert.strictEqual(currentURL(), '/posts');
});
});
Both can coexist in the same suite — convert at your own pace.
Selectors
ember-test-selectors (the data-test-* strip-on-build addon) was already common in 2.x. If your codebase doesn't use it, adding data-test-* selectors during the migration is one of the higher-leverage things you can do — your tests become independent of the CSS overhaul that almost always accompanies a major upgrade.
Mirage — already a thing
ember-cli-mirage was the standard mock-server addon throughout 2.x. The factory/route-handler API has been remarkably stable; in most cases, your 2.x Mirage setup will keep working after the upgrade with only minor tweaks.
Common 2.x test mistakes
| Symptom | Cause | Fix |
|---|---|---|
| Test passes locally, fails in CI | Order-dependent state (a previous test's controller bled in). | Tear down state in afterEach; convert to module + hooks. |
andThen block runs before async work finishes |
wait() doesn't see the work because it's outside the run loop. |
Wrap in Ember.run(...) or register a test waiter. |
| jQuery DOM lookups break after CSS refactor | Selectors are class-based. | Add data-test-* and select by those. |
this.subject() returns undefined |
Missing needs: declaration. |
Add the dependency factory name. |
| Acceptance test hangs forever | Unawaited fetch or setTimeout not registered with a waiter. |
Register a waiter via Ember.Test.registerWaiter(...). |
Verification
- You recognize
moduleFor*as the 2.x setup mechanism. - You can read
andThen-style tests as the pre-awaitshape. - You know
this.$(...)is jQuery andfind(...)is the modern equivalent. - You can convert one 2.x test file to the modern
module + setupApplicationTestshape. -
ember-qunit@^4is in the project'spackage.jsonif you plan to mix styles.
See also
ember-2-classic-patterns— the 2.x dialect under test.ember-2-to-3-migration— when to convert tests during a migration.- Modern reference:
ember-testing.