Alisdair McDiarmid is a software engineer based in Vancouver, British Columbia.

Ember component integration tests

The best way to build Ember apps is to focus on models, routes, and components. Models and routes are easy to unit test: we can stub out their collaborators and test to the interface.

But components are UI, so unit testing doesn't really work. You need browser events, and controller actions, and other sub-components, and templates. It gets messy.

Good news! As of a few weeks ago, we have a great way to properly test Ember components. Integration testing lets you render your component in a tiny isolated template, hook it up to its dependencies and collaborators, and test all of its functionality. Now you don't have to resort to acceptance tests to cover complex behaviour, and you can build your app from a collection of well-tested components.

This post describes how to go about this, including:

Show Me The Code!

To show you what I mean, I've built a tiny Ember app (code on GitHub) with a really simple alert banner component. Here's what one of the tests looks like:

test('closing an alert', function(assert) {
  assert.expect(2);

  var hello = Alert.create({ text: 'Hello, world!' });
  this.set('helloAlert', hello);

  this.render(hbs`
    {{alert-banner alert=helloAlert closeAction='assertClosed'}}
  `);

  var $button = this.$('.alert-banner button');
  assert.equal($button.length, 1);

  this.on('assertClosed', a => assert.deepEqual(a, hello));
  $button.click();
});

Wait! Look at this bit from the middle of the test:

this.render(hbs`
  {{alert-banner alert=helloAlert closeAction='assertClosed'}}
`);

It's a tiny Handlebars template, right in your test code! Integration tests let you test your components in almost exactly the same way you'll use them, without booting up your entire application.

Why Integration Testing Matters

Until recently, there were two types of tests for Ember modules: acceptance tests, and unit tests. Acceptance tests boot your entire application, and allow you to test everything, from URLs to CSS. Unit tests load individual modules or functions without the rest of your app, and allow you to test algorithms or computed properties much more easily.

Recent changes made to the ember-test-helpers add a third class of test, which sits between the other two. Integration tests aren't fully isolated, but they don't load your entire app either. They run faster than acceptance tests, yet allow you to render templates and handle actions.

Fast tests are important, but there are other advantages to Ember component integration testing.

Unit Testing Components is Unrealistic

Unit tests require you to instantiate your Ember components in JavaScript, like this example from the Ember.js guides:

test('changing colors', function(assert) {
  var component = this.subject();

  Ember.run(function() { component.set('name','red'); });
  assert.equal(this.$().attr('style'), 'color: red;');

  Ember.run(function() { component.set('name', 'green'); });
  assert.equal(this.$().attr('style'), 'color: green;');
});

You will never use an Ember component like this in your production code. Components are integrated into your app with templates, and you don't set attributes on them directly either. With integration testing, you could write the above test like this:

test('changing colors', function(assert) {
  this.set('colorName', 'red');

  this.render(hbs`{{pretty-color name=colorName}}`);

  assert.equal(this.$('.pretty-color').attr('style'),
               'color: red;');

  this.set('name', 'green');
  assert.equal(this.$('.pretty-color').attr('style'),
               'color: green;');
});

There two small but important differences here:

Components Are Never Truly Isolated

No matter how well you design your components, in real apps they will always have subtle interactions with their environments. Maybe via sub-components, maybe other elements on the page. Unit tests can't verify this behaviour, because there is no template, no page, and no other component to collaborate with.

Integration testing can allow you to test components that interact with other parts of the DOM. Testing components that use the awesome ember-wormhole component would be basically impossible without integration tests. Any component which takes a block and yields context to its parent can't be tested without integration testing. These use cases are just as common as simpler components, but now you can test them as well!

Quick Start

Convinced? Great! Here's how to get started.

You'll need to be using ember-cli and at least Ember 1.10. First, upgrade to ember-qunit 0.4.0, ember-cli-qunit 0.3.14, and install ember-cli-htmlbars-inline-precompile 0.1.1:

bower install --save ember-qunit#0.4.0
npm install --save-dev ember-cli-qunit@0.3.14
npm install --save-dev ember-cli-htmlbars-inline-precompile@0.1.1

Then create an integration test file for your component. Here's a quick stub for tests/integration/components/my-component-test.js:

import hbs from 'htmlbars-inline-precompile';
import { moduleForComponent, test } from 'ember-qunit';

moduleForComponent('my-component', {
  integration: true
});

test('renders text', function(assert) {
  this.render(hbs`{{my-component text="Hello!"}}`);

  var $component = this.$('.my-component');
  assert.equal($component.text(), 'Hello!',);
});

Example: Building an Alert Banner Component

To show in more detail how to write integration tests, I've published a sample Ember.js project with a live demo of the component in action. Below is a walkthrough of how I built it, with three example tests.

(Note: this component is just a teaching toy. If you want an alert banner component, try ember-cli-flash, which is awesome.)

First Test

An alert banner is about as simple a component as I could think of. At its simplest, it's just a <div> with some text:

<div class="alert-banner">Tweet published!</div>

Here's the commit of the first version of the component. As you can see, there really isn't much to it. This is the component JavaScript:

import Ember from 'ember';

export default Ember.Component.extend({
  classNames: ['alert-banner'],

  alert: null,
  text: Ember.computed.alias('alert.text'),
});

The template is really simple:

{{text}}

At this stage, testing the component is also trivial:

test('renders text', function(assert) {
  this.set('helloAlert', Alert.create({ text: 'Hello, world!' });

  $this.render(hbs`
    {{alert-banner alert=helloAlert}}
  `);

  var $alert = this.$('.alert-banner');
  assert.equal($alert.length, 1);
  assert.equal($alert.text().trim(), 'Hello, world!');
});

There are a few things worth noting, though:

Testing Actions

Components that display bound content are fine, but interactive components are much more interesting. Let's add an optional close button to our alert, which will fire an action to its target.

This commit has the changes for the whole app, including the tests. In our controller, we have this action:

actions: {
  removeAlert(alert) {
    this.get('alerts').removeObject(alert);
  }
}

If we want our component to fire this action, we render it like this:

{{alert-banner alert=helloAlert closeAction='removeAlert'}}

Our component template now has a conditional close button:

{{#if closeAction}}
  <button {{action 'closeAction'}} aria-label="Close" class="close">
    <span aria-hidden="true">×</span>
  </button>
{{/if}}
{{text}}

And its closeAction just sends back the configured action to its target, along with the alert object itself of course:

actions: {
  closeAction() {
    this.sendAction('closeAction', this.get('alert'));
  }
}

Testing this with an integration test is straightforward:

test('closing an alert', function(assert) {
  assert.expect(2);

  var hello = Alert.create({ text: 'Hello, world!' });
  this.set('helloAlert', hello);

  this.render(hbs`
    {{alert-banner alert=helloAlert closeAction='assertClosed'}}
  `);

  var $button = this.$('.alert-banner button');
  assert.equal($button.length, 1);

  this.on('assertClosed', a => assert.deepEqual(a, hello));
  $button.click();
});

We render the template just as we would in the app, and then set up an action handler on the test object with an assertion inside it. Finally we click the button, and our assertions run.

Using Blocks

Alert banners are boring if all they can render is plain text. So let's add support for yielding to a block with the alert as a parameter. Using the component would look like this:

{{#alert-banner alert=errorAlert as |text|}}
  <h3>Save Failed!</h3>

  <p class="text-warning">{{text}}</p>

  <button {{action 'retry'}}>Retry?</button>
{{/alert-banner}}

Here's the commit with the changes required to support this. The code change for the component is small: just calling {{yield text}} from the template.

The test is also really easy to write. We just update the template string in our test according to the real use case, then make some jQuery-selector powered assertions about the DOM:

test('with block for rendering text', function(assert) {
  this.set('panicAlert', Alert.create({ text: 'Panic!' });

  this.render(hbs`
    {{#alert-banner alert=panicAlert as |text|}}
      <h3 class="text-danger">Something Went Wrong</h3>

      <p>{{text}}</p>
    {{/alert-banner}}
  `);

  var $h3 = this.$('.alert-banner h3.text-danger');
  assert.equal($h3.text().trim(), 'Something Went Wrong');

  var $p = this.$('.alert-banner p');
  assert.equal($p.text().trim(), 'Panic!');
});

You can see the full source code for this app and component on GitHub, and try out the component at the demo page.

Limitations

If you're using Ember 1.10–1.12, you can't integration test components which render links with link-to. This is a known issue, and a fix has been merged into the Ember 1.13 and 2.0 releases. Good motivation to upgrade!

Finally, a few links and thanks: