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:
- Advantages of integration testing over unit testing;
- How to start integration testing your components;
- Sample project with a live demo, a toy component, and integration tests.
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:
We don't have a reference to the component, just like in the app. We're not setting properties on the component, just like in the app. The component's properties are bound to its context, just like in the app.
There's an example right in the test of how you'd use the component. You render it and bind a
name
attribute. Tests are the best kind of documentation for developers, and having a sample template in the tests is a great way of explaining how to use the component.
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:
- The component is rendered via a template. Thanks to ember-cli-htmlbars-inline-precompile, we can specify the HTMLbars template using a tagged "
hbs
" template string. If you're curious, Clemens Müller wrote more on how this works under the hood.
We find the component's element using
this.$('.alert-banner')
. This is different from unit tests, wherethis.$()
returns a jQuery object for the component element. Here we're rendering an entire template, so the top level element is a container.Our component runs in the context of the test. When we bind
alert=helloAlert
in the HTMLbars template, this means that the component'salert
property is bound to thehelloAlert
property of the test object.
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!
Links and Credits
Finally, a few links and thanks:
- Edward Faulkner, who built the integration testing feature;
- Robert Jackson and Dan Gebhardt, who maintain ember-test-helpers;
- Clemens Müller, who built the Babel plugin;
- And my coworkers Justin Brown and Alvin Crespo, who introduced this feature to me.