The Ember.js testing guide, I made for myself

Sarbbottam Bandyopadhyay
Jun 14 · 14 min read

When I started with Ember.js in mid-2017, I had a hard time with testing. I never got my head around with the syntax. For example, consider the following code snippet.

import { moduleFor, test } from 'ember-qunit';

moduleFor('model:some-thing', 'Unit | some thing', {
unit: true
});

test('should correctly concat foo', function(assert) {
const someThing = this.subject();
someThing.set('foo', 'baz');

assert.equal(someThing.get('computedFoo'), 'computed baz');
});

I didn’t comprehend the purpose of moduleFor; how this.subject() referred to the instance being considered for testing, are just to name a few.

The same test in newer syntax looks like so:

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Service | some-thing', function(hooks) {
setupTest(hooks);

test('should correctly concat foo', function(assert) {
const someThing = this.owner.lookup('service:some-thing');
someThing.set('foo', 'baz');

assert.equal(someThing.computedFoo, 'computed baz');
});
});

My productivity in writing Ember.js test code improved remarkably with the newer syntax. Other than this.owner.lookup('service:some-thing') I could comprehend the code with my existing JavaScript knowledge. References and usages are mostly explicit.

With respect to the newer syntax, I have been making notes for myself and been referring in need. I feel others might find it useful too and thus sharing it publicly.


Table of Contents

Types of tests

There are three types of tests in the ember ecosystem, namely:

  • unit
  • integration
  • acceptance

I rely on ember-cli, the primary command line tool in Ember.js ecosystem, to scaffold any files in an Ember.js application, tests files are no different. Moreover, ember-cli automatically generates the corresponding tests files when generating the desired source files.

For example:

$ ember g service some-service
installing service
create app/services/some-service.js
installing service-test
create tests/unit/services/some-service-test.js
$ ember g route some-route
installing route
create app/routes/some-route.js
create app/templates/some-route.hbs
updating router
add route some-route
installing route-test
create tests/unit/routes/some-route-test.js
$ ember g helper some-helper
installing helper
create app/helpers/some-helper.js
installing helper-test
create tests/integration/helpers/some-helper-test.js
$ ember g component some-component
installing component
create app/components/some-component.js
create app/templates/components/some-component.hbs
installing component-test
create tests/integration/components/some-component-test.js

If a functionality needs to be tested by rendering the UI, it is tested via integration tests and if no rendering is needed then the functionality is tested via a unit test.

Thus, components, helpers, are tested via integration tests and services, routes, et al are tested via unit tests.

ember-cli creates the desired type of test file while generating the corresponding source file. For services, routes et al, it creates unit tests and for helpers, components, it creates integration tests by default.

acceptance tests are not associated with any file but the application and are not automatically generated by ember-cli while generating the source code. acceptance tests are generated explicitly, like so:

$ ember g acceptance-test some-route
installing acceptance-test
create tests/acceptance/some-route-test.js

acceptance tests test the application from a user’s point of view by performing the actions like visiting a route, interacting with the rendered elements and so on.

Similar to the acceptance test blueprint, we can also use component-test, helper-test, et al blueprints to generate desired tests files.

$ ember g component-test some-component  
installing component-test
create tests/integration/components/some-component-test.js
$ ember g helper-test some-helper
installing helper-test
create tests/integration/helpers/some-helper-test.js

⚠️ Please note ember-cli does not generate unit tests for components and helpers by default (when generating the corresponding source code or when generating the test files explicitly). However, we can pass -u or (--test-type=unit) option to create unit tests for components and helpers.

$ ember g component-test some-component -u 
installing component-test
create tests/unit/components/some-component-test.js
$ ember g helper-test some-helper -u
installing helper-test
create tests/unit/helpers/some-helper-test.js

📝 I prefer to stick to the defaults and thus never unit test components and helpers.

Anatomy of a test file

Setups and Teardowns

ember-qunit provides the following common setup and teardown out of the box

📝 We can also create our own setups, to abstract away any repeated setups and teardowns, for example:

// custom-setup-declaration.js
export function setupSomething(hooks) {
hooks.beforeEach(function() {
// perform setups
});
hooks.afterEach(function() {
// perform teardowns
});
}

Structure of test files

The structure of any test file looks like so:

import { module, test } from 'qunit';
/*
* setup* is either of
* setupTest, setupRenderingTest, and setupApplicationTest
*/
import { setup* } from 'ember-qunit';
/* import custom setup if needed */
import { setupSomething } from '<app-context>/path/to/custom-setup-declaration.js';
/*
* 'module name' and 'test name' are used to uniqely identify
* a test block
*/
module('module name', options, function(hooks) {
setup*(hooks);
/* custom setup if needed */
setupSomething(hooks);
/* custom setup and tear downs */
hooks.before(function() { ... });
hooks.after(function() { ... });
hooks.beforeEach(function() { ... });
hooks.AfterEach(function() { ... });

test('test name', function(assert) {
...
});
});

📝 app-context is the value of modulePrefix specified in the application’s environment.js.

📝 module is used to group similar tests together.

A basicunit test would look like so:

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | some-service', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
const someService = this.owner.lookup('service:some-service');
assert.ok(someService);
});
});

A basic integration test would look like so:

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Helper | some-helper', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
this.set('inputValue', '1234');
await render(hbs`{{some-helper inputValue}}`);
assert.equal(this.element.textContent.trim(), '1234');
});
});

integration tests uses utilities like render from @ember/test-helpers and hbs from htmlbars-inline-precompile.

The test functions in integration tests are marked as async as we await on render which returns a promise.

A basicacceptance test would look like so:

import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit, currentURL } from '@ember/test-helpers';
module('Acceptance | some route', function(hooks) {
setupApplicationTest(hooks);
test('visiting /some-route', async function(assert) {
await visit('/some-route');
assert.equal(currentURL(), '/some-route');
});
});

acceptance tests uses utilities like visit and currentURL from @ember/test-helpers.

The test functions in acceptance tests are also marked as async as we await on visit which returns a promise.

Overview of the three types of tests

Unit tests

Unit tests focus on testing the functionalities of a single unit/file.

A unit test may be testing the functionality of an instance that depends on the container, which is Ember’s Dependency Injection system. For example, a controller, or a service, or a route.

If the desired unit depends on the application container, then it needs to be looked up using this.owner.lookup(). this.owner is the ApplicationInstance. Please refer the API documentation and Getting an Application Instance from a Factory Instance for details on ApplicationInstance.

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | some-service', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
const someService = this.owner.lookup('service:some-service');
assert.ok(someService);
});
});

The argument passed to the this.owner.lookup() is a : delimited string, where the first part identifies the type of instance and the second part identifies the name of the instance. In the above example type is a service and name is some-service.

If we were to test a route named some-service, we would have looked it up like so:

const someService = this.owner.lookup('service:some-service');

Similarly to test a controller name some-controller, we would look it up like so:

const someController = this.owner.lookup('controller:some-controller');

A unit test may also be testing the functionality of an instance that does not depend on the Ember’s Dependency Injection system, for example, a utility.

$ ember g util some-util
installing util
create app/utils/some-util.js
installing util-test
create tests/unit/utils/some-util-test.js

To test a utility or any functionality that does not depend on Ember’s Dependency Injection system, we can directly import the functionality and use it like so:

import someUtil from '<app-context>/utils/some-util';
import { module, test } from 'qunit';
module('Unit | Utility | some-util', function(hooks) {
test('it works', function(assert) {
assert.ok(someUtil());
});
});

📝 app-context is the value of modulePrefix specified in the application’s environment.js.

Integration tests

Integration tests are used for testing functionalities that need to be rendered, primarily components and helpers.

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Helper | some-helper', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
this.set('inputValue', '1234');
await render(hbs`{{some-helper inputValue}}`);
assert.equal(this.element.textContent.trim(), '1234');
});
});

Using the render function from @ember/test-helpers any valid handlebar fragment can be rendered and using DOM Query Helpers and DOM Interaction helpers from @ember/test-helpers, nodes can be fetched and interacted respectively.

Acceptance tests

Acceptance tests simulate end users behavior. In acceptance tests, the application is always tested by visiting a desired route and then interacting with it in the same way that a real user would have.

import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit, currentURL } from '
@ember/test-helpers';
module('Acceptance | some route', function(hooks) {
setupApplicationTest(hooks);
test('visiting /some-route', async function(assert) {
await visit('/some-route');
assert.equal(currentURL(), '/some-route');
});
});

Using the visit function from @ember/test-helpers any valid route can be rendered and using DOM Query Helpers and DOM Interaction helpers from @ember/test-helpers, the rendered UI can be operated in a similar way as an end user.

Unit testing Components and Helpers

As stated earlier, ember-cli does not generate unit tests for components and helpers by default when generating the corresponding source code or when generating the test files explicitly. I prefer to stick to the defaults and thus never unit test components and helpers.

However, we can pass -u or (--test-type=unit) option to create unit tests for components and helpers like so.

$ ember g component-test some-component -u 
installing component-test
create tests/unit/components/some-component-test.js
$ ember g helper-test some-helper -u
installing helper-test
create tests/unit/helpers/some-helper-test.js

In order to unit test a component we can create an instance of the desired component using this.owner.factoryFor('component:some-component').create({}) and perform any assertions.

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Component | some-component', function(hooks) {
setupTest(hooks);
test('some aspect of it', function(assert) {
const someComponent = this.owner.factoryFor('component:some-component').create({ /* desired attributes */});
assert.equal(someComponent.someMethod(), 'desired output', 'it works');
});
});

📝 The factoryFor method could be used for other factories like services, routes, controller etc.

Executing tests

ember test command will run all the available test against a Headless Chromium instant.

Executing a single test

To run a single test we can pass the --filter='filter string' to the ember test command.

Executing tests against the local server

I personally prefer to run an individual test against the running instance of the local server.

If the local ember application can be accessed at http://host:port/ then the tests can be executed at http://host:port/tests. We can also pass the desired filter string as a query parameter like so:

http://host:port/tests?filter=filter string

Or we can enter it in the filter textbox displayed at http://host:port/tests

test-helper.js

All the desired environment and prerequisites to run the tests are set up in test-helper.js.

The default contents of the test-helper.js looks like so:

import Application from '../app';
import config from '../config/environment';
import { setApplication } from '@ember/test-helpers';
import { start } from 'ember-qunit';
setApplication(Application.create(config.APP));
start();

Configuration options can be passed to start() in order to enable/disable certain extended features. For example:

start({
setupTestIsolationValidation: true
});

📝 I plan to write a separate post on the options that can be passed to the start, method imported from ember-qunit.

testem.js

ember-cli uses testem as the default test runner. All the desired configuration for testem can be found at the project’s testem.js file. The default configuration for testem.js is like so:

module.exports = {
test_page: 'tests/index.html?hidepassed',
disable_watching: true,
launch_in_ci: [
'Chrome'
],
launch_in_dev: [
'Chrome'
],
browser_args: {
Chrome: {
ci: [
// --no-sandbox is needed when running Chrome inside a container
process.env.CI ? '--no-sandbox' : null,
'--headless',
'--disable-gpu',
'--disable-dev-shm-usage',
'--disable-software-rasterizer',
'--mute-audio',
'--remote-debugging-port=0',
'--window-size=1440,900'
].filter(Boolean)
}
}
};

Please refer the testem documentation for further details.

Stubbing

In unit and integration tests, often the dependencies are needed to be stubbed.

Let’s consider the following example:

// app/components/some-component.jsimport Component from '@ember/component';
import { inject as injectService } from '@ember/service';
export default Component.extend({
someService: injectService('some-service'),
actions: {
someMethod() {
this.someService.someMethod();
}
}
});

In the above code someMethod of some-component.js invokes the someMethod method of some-service.js.

We can either stub the someMethod of some-service or stub the some-service completely.

Stubbing only the needed

All we need is to test, that someMethod of the some-component when invoked, in turn, invokes the someMethod of the some-service.

Thus we need to only stub the someMethod of the some-service.

In order to stub the someMethod method of some-service.js, we need to look up the instance of the someService from the Ember’s Dependency Injection system’s container.

const someService = this.owner.lookup(“service:some-service”);

Then cache the someMethod of someService.

const someMethod = someService.someMethod;

Replace the someMethod of someService with the desired stub. This stub will perform the assertion and then update the someService.someMethod with the original implementation.

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | some-component', function(hooks) {
setupRenderingTest(hooks);
test('stubbing someService.someMethod', async function(assert) {
const someService = this.owner.lookup('service:some-service');
const someMethod = someService.someMethod;
someService.someMethod = () => {
// assertion
assert.ok('someMethod was called');
// restore the original implementation
someService.someMethod = someMethod;
};

await render(hbs`{{some-component}}`);
await click('[data-test-someMethod]');
});
});

⚠️ If someService.someMethod is not restored to its original implementation, it may impact any subsequent tests if they are relying on the original implementation of someService.someMethod.

Stubbing an instance completely

In order to stub the some-service completely we need to register the stub and perform the assertion in the desired method of the stub.

const someServiceStub = Service.extend({
someMethod() {
assert.ok('someMethod was called');
}
});
this.owner.register('service:some-service', someServiceStub);

📝 this.owner.register is isolated to each test block and does not affect other test blocks thus no stub restoration is needed.

import Service from '@ember/service';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | some-component', function(hooks) {
setupRenderingTest(hooks);
test('stubbing whole service', async function(assert) {
const someServiceStub = Service.extend({
someMethod() {
assert.ok('someMethod was called');
}
});
this.owner.register('service:some-service', someServiceStub);

await render(hbs`{{some-component}}`);
await click('[data-test-someMethod]');
});
});

The above tests serve their purpose but do not follow the AAA (Arrange, Act, Assert) pattern. Though AAA (Arrange, Act, Assert) pattern is primarily suggested for unit testing, it can also be used for integration and acceptance testing, sometimes with a little variation like Arrange, Act, Act, Assert, Arrange, Act, Assert, Act, Assert and so on.

Sinon.js can be useful to achieve AAA (Arrange, Act, Assert) pattern.

📝 Also, Sinon.js exposes an API to restore the stubs with much lesser code. Thus it is better to stub only the needed when using Sinon.js.

Stubbing only the needed using Sinon.js

In order to use sinon.js, first, we need to install it:

yarn add sinon

And then we will make sinon available in the global scope by updating the project’s ember-cli-build.js like so:

'use strict';const EmberApp = require('ember-cli/lib/broccoli/ember-app');module.exports = function(defaults) {
let app = new EmberApp(defaults, {});
if (process.env.EMBER_ENV !== 'production') {
app.import('node_modules/sinon/pkg/sinon.js');
}
return app.toTree();
};

📝 We are importing sinon.js only for the non-production environment so that we don’t increase the production bundle’s size, by including unnecessary code in it.

The above-discussed test using sinon.js could be written as:

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | some-component', function(hooks) {
const sandbox = sinon.createSandbox();
setupRenderingTest(hooks);
hooks.afterEach(function() {
sandbox.restore();
});
test('stubbing someService.someMethod', async function(assert) {
const someService = this.owner.lookup('service:some-service');
const someMethodStub = sandbox.stub(someService, 'someMethod');
await render(hbs`{{some-component}}`);
await click('[data-test-someMethod]');
assert.ok(someMethodStub.called, 'someMethodStub.called');
});
});

The Arrange in the above test is

const someService = this.owner.lookup('service:some-service');
const someMethodStub = sandbox.stub(someService, 'someMethod');
await render(hbs`{{some-component}}`);

Act is

await click('[data-test-someMethod]');

and the Assert is

assert.ok(someMethodStub.called, 'someMethodStub.called');

📝 Please note that we are creating a sandbox and restoring it after every test.

However, instead of manually creating and restoring sandboxes, we could use ember-sinon-sinoff which will take care of the creation and restoration of the sandboxes by itself without any extra code and make will make sandbox available in the test instance via this.sandbox.

Using ember-sinon-sinoff

Thus using ember-sinon-sinoff, the previously discussed test would look like so:

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | some-component', function(hooks) {
setupRenderingTest(hooks);
test('stubbing someService.someMethod', async function(assert) {
const someService = this.owner.lookup('service:some-service');
const someMethodStub = this.sandbox.stub(someService, 'someMethod');
await render(hbs`{{some-component}}`);
await click('[data-test-someMethod]');
assert.ok(someMethodStub.called, 'someMethodStub.called');
});
});

Please refer to ember-sinon-sinoff documentation for further details and integration strategies.

Stubbing API responses

If an acceptance test depends on API calls we need to stub the API responses. To stub API responses, we can use ember-cli-pretender.

Once ember-cli-pretender is installed, we can import and use it in the tests. For example, if visiting /some-route, there is an /someAPI call, we can stub the /someAPI call like so:

import { module, test } from 'qunit';
import { visit, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import Pretender from 'pretender';module('Acceptance | pretender', function(hooks) {
setupApplicationTest(hooks);
test('visiting /some-route', async function(assert) {
const server = new Pretender();
server.get('/someAPI', () => [
200,
{"Content-Type": "application/json"},
JSON.stringify({ status: 'ok' })
]);
await visit('/some-route');
assert.equal(currentURL(), '/some-route');
assert.dom('[data-test-status]').hasText('ok');
});
});

ember-cli-pretender is great, but if we are using ember-data to consume API ember-cli-mirage sounds a better match.

For more information on ember-cli-mirage please refer to the ember-cli-mirage documentation, it has done a great job in documenting the usage in detail.

Playground

You can check out the companion app for this post at codesandbox.io and the test executions here.


I refer to this documentation whenever I am in need and update it with any new learnings. I hope if you are like me, you will find this writeup useful. Please feel free to leave your feedback with suggestions, concerns or appreciation.

Acknowledgment

I am thankful to my colleagues Walter Shen, Eric Huang and Steve Calvert for always being there to clarify any of my doubts with ember testing and helping me to comprehend the concepts of ember testing better.

I am thankful to Robert Jackson for the championing the simplify-qunit-testing-api RFC, which has improved developer experience in writing Ember.js tests remarkably. I am also thankful to Mike North for the PR that enabled Ember.js support in codesandbox, which has made it delightful to develop the companion code for this write-up.

Last but not least, I am must thank Walter Shen and Eric Huang again for reviewing this post and you for reading it. ☕️

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade