How to Test Ember Components

Ember is all about routes, components and services.

Components take the stage, interacting in multiple ways within our application. Wouldn’t it be great to have to have a solid test suite for them?

Do components demand acceptance or integration tests? Which helpers are available for each kind? How do we set up moduleForComponent? What is the right way to test a component in isolation?

Integrating

At an algorithm level, unit testing is the king.

From a user perspective (loading URLs, displaying, interacting), acceptance testing is the king.

Integration testing is somewhere in between.

As UI elements, components are not pure functions. But they do have an input (data, actions) and an output (HTML, action handling):

{{my-component item=model action=(action externalAction)}}

Integration tests are thus the most suitable for components. They:

While they are not a substitute for acceptance tests, they give us confidence about the building blocks of our system.

Integration testing is significantly faster than acceptance testing as it doesn’t require booting the entire app. I like to start with these because they provide the best bang for the buck.

Testing a Wine Stock component

We hereby introduce our “wine stock” component, which will be used throughout a Wine Bar application.

It allows to sell a certain amount of bottles of some wine and show the updated stock.

We’ll have to assert:

To follow along, let’s create an Ember app:

$ ember new winebar
$ cd winebar

Install the latest QUnit, Ember’s default test framework:

$ ember install ember-cli-qunit ember-cli-es5-shim

The ES5-shim is necessary for PhantomJS to work. When prompted, always choose the newest version (last option).

All set?!

Step one is to generate our wine-stock component:

$ ember generate component wine-stock

Along with Javascript + Handlebars, we get an integration test file at tests/integration/components/wine-stock-test.js.

Let’s run it!

$ ember test --server -m 'Integration | Component | wine stock'

What is this exactly doing? ember test --server will spin up a command-line utility that watches for changes in our tests and code. With the -m switch we tell it to only run tests for this particular module.

All green!

Ember will expose a test runner at http://localhost:4200/tests. I recommend against using it.

This will run tests in development mode which (a) will have a different configuration probably not suited to testing, (b) may end up persisting unwanted mock data in the database.

Always use ember test --server (or ember t -s in its short form). Want to see them in a browser? Check out http://localhost:7357/.

Tests first

In the best TDD spirit, let’s start by expecting functionality in our tests, letting them fail and then implementing what’s necessary to make them pass.

ember test --server is running, right?

First thing we want to make sure, is that the component has a title. So let’s open up the test and make it look exactly like this:

// tests/integration/components/wine-stock-test.js

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

moduleForComponent('wine-stock', 'Integration | Component | wine stock', {
  integration: true
});

test('it renders empty', function(assert) {
  assert.expect(1);

  this.render(hbs`{{wine-stock}}`);

  // check title is correct

  assert.equal(this.$('h1').text(), 'Wine Stock');

});

If fails, that’s perfect! We are calling the component in exactly the same way we’d call it from another template. After rendering, we have access to the component as this.

Ember best practices delivered straight to your inbox? Tell me where:

(One e-mail every month. No BS. Unsubscribe anytime!)

Let’s make it pass, shall we? Moving onto our component’s template…

{{! app/templates/components/wine-stock.hbs }}

<h1>Wine Stock</h1>

It’s passing! We fulfilled the test’s expectation to find an <h1> with the text “Wine Stock”.

Let’s enjoy our moment of bliss before we decide to break things again. That’s right, we are moving forward with our next requirement.

Mock data

To make this real, let’s drink some wine––I mean, let’s pass in some wines.

We’ll add a new test to our file:

// tests/integration/components/wine-stock-test.js

// ...

test('it renders wines in dropdown and list', function(assert) {
  assert.expect(1);

  const model = makeWineObjects(12);

  this.set('model', model);
  this.render(hbs`{{wine-stock wines=model}}`);

  // ...

});

It fails: Can't find variable: makeWineObjects

Where does makeWineObjects come from?

We generate a test helper that we’ll use to create a bunch of mock data:

$ ember g test-helper make-wine-objects

This is a quick sample I just came up with, feel free to adjust!

// tests/helpers/make-wine-objects.js

import Ember from 'ember';

export default function makeWineObjects(total) {

  const names = [
    'Zisola Sicilia',
    'Ribera del Duero',
    'Clos Fourtet St.-Emilion',
    'Blandy Bual Madeira',
    'Baer Ursa',
    'Tenet Syrah Columbia Valley',
    'Oddero Barolo',
    'Lavau Gigondas',
    'Duorum Douro',
    'Philippe Alliet Chinon',
    'Orin Swift Machete',
    'Finca Flichman'
  ];
  const wineObjects = [];

  for (let i=0; i < total; i++) {

    const wineObject = Ember.Object.extend({

      name: Ember.computed(() => names[i]),
      quantity: Ember.computed(() => getRandomInt(10, 300)),

      toString() {
        return `${this.get('name')} (${this.get('quantity')})`;
      }

    }).create();

    wineObjects.push(wineObject);

  }

  return wineObjects;

}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

In integration tests, we cannot use or access fillIn, click, etc as available to acceptance tests. this.$() is our interaction mechanism with the DOM.

For more powerful mock objects, check out Mocking Integration Test Data with Ember CLI Mirage.

It’s fully compatible with the Winebar app at this stage – simply use the Mirage helper instead of makeWineObjects!

Back to our test…

The wine models are now ready to be supplied as argument to the component.

The component displays all the wines (and quantity = remaining stock) in a dropdown and in a plain unordered list, too:

The makeWineObjects helper should be imported, of course. Time for more assertions!

// tests/integration/components/wine-stock-test.js

import makeWineObjects from '../../helpers/make-wine-objects';

// ...

test('it renders wines in dropdown and list', function(assert) {
  assert.expect(5);

  const model = makeWineObjects(12);

  this.set('model', model);
  this.render(hbs`{{wine-stock wines=model}}`);

  // check wines are included in dropdown

  const $dropdown = this.$('.dropdown');

  assert.equal($dropdown.find('option:eq(0)').text(), model[0].toString());
  assert.equal($dropdown.find('option:eq(1)').text(), model[1].toString());

  // check all wines are listed

  const $list = this.$('.wines');

  assert.equal($list.find('.wine:eq(0)').text(), model[0].toString());
  assert.equal($list.find('.wine:eq(8)').text(), model[8].toString());
  assert.ok($list.find('.wine').length);

});

The assert module provides us with different assertion methods such as equal, deepEqual, ok, notOk and so on.

We have used jQuery selectors (via this.$()) to interact with our component DOM.

In general, it’s good practice to select class names rather than elements such as ul, div, as these may be changed in the template. Depending on your choices, class names tend to convey more meaning.

At this point tests should be failing. Let’s develop the necessary features to see the green ticks once again!

Ember does not have a decent dropdown helper. We’ll use emberx-select that can simply be installed via ember install emberx-select.

{{! app/templates/components/wine-stock.hbs }}

<h1>Wine Stock</h1>

<form>
  {{#x-select value=selectedWine class="dropdown" }}
    {{#each wines as |wine|}}
      {{#x-option value=wine}}{{ wine.name }} ({{ wine.quantity }}){{/x-option}}
    {{/each}}
  {{/x-select}}
</form>

<ul class="wines">
  {{#each wines as |wine|}}
    <li class="wine">{{ wine.name }} ({{ wine.quantity }})</li>
  {{/each}}
</ul>

All tests should be passing now. Correct?

How do we debug tests?

Directly invoking debugger; in our tests! It can be accessed through dev tools in any browser at http://localhost:7357/.

Testing actions

Next, we want to check that actions we send up are correctly invoked. As the component allows to input wine sales which decrease the stock, it will send up an action with the updated models.

Test first:

// tests/integration/components/wine-stock-test.js

// ...

test('it reacts to updates', function(assert) {
  assert.expect(1);

  const model = makeWineObjects(3);

  const firstWine = model[0],
    lastWine = model[2],
    firstQty = parseInt(firstWine.get('quantity')),
    lastQty = parseInt(lastWine.get('quantity'));

  // check external action with correct values

  this.set('model', model);
  this.set('updateStock', (model) => {
    const json = model.getProperties('name', 'quantity');
    const name = firstWine.get('name');
    assert.deepEqual(json, { name: name, quantity: firstQty-3 });
  });
  this.render(hbs`{{wine-stock wines=model sellWines=(action updateStock)}}`);

  this.$('.quantity').val(3).trigger('change');
  this.$(`option:contains("${firstWine.get('name')}")`).prop('selected', true).trigger('change');
  this.$('.button').click();

});

There’s a lot going on here! Simulating user interaction with jQuery!

We are planning to have a .quantity input field and a .button that submits the form. Once that form is submitted, an external action updateStock will be called.

In this test, we set the action directly via this.set('updateStock', ...) and make all relevant assertions within the callback. Here we chose deepEqual to compare a data structure. Notice that the quantity becomes the original minus 3 (as three bottles were sold).

assert.expect tells QUnit how many assertions to expect. Make certain that you update this value accordingly.

In order to comply with this (again failing) test, we should work on our component.

{{! app/templates/components/wine-stock.hbs }}

<h1>Wine Stock</h1>

<form {{action "submit" on="submit"}}>

  {{#x-select value=selectedWine }}
    {{#each wines as |wine|}}
      {{#x-option value=wine}}{{ wine.name }} ({{ wine.quantity }}){{/x-option}}
    {{/each}}
  {{/x-select}}

  {{input class="quantity" type="number" value=selectedQuantity}}
  <button class="button">Sell</button>
</form>

<ul class="wines">
  {{#each wines as |wine|}}
    <li class="wine">{{ wine.name }} ({{ wine.quantity }})</li>
  {{/each}}
</ul>

Handling the sell action in Javascript-land:

// app/components/wine-stock.js

export default Ember.Component.extend({

  actions: {
    submit() {
      const wine = this.get('selectedWine');
      const newQty = this.get('selectedQuantity');
      wine.decrementProperty('quantity', newQty);
      this.attrs.sellWines(wine);

      // reset form
      this.setProperties({ selectedWine: null, selectedQuantity: null });

    }
  }

});

Success, tests pass again!

For safety, let’s add a few more assertions…

// tests/integration/components/wine-stock-test.js

// ...

test('it reacts to updates', function(assert) {
  assert.expect(3);

  const model = makeWineObjects(3);

  const firstWine = model[0],
    lastWine = model[2],
    firstQty = parseInt(firstWine.get('quantity')),
    lastQty = parseInt(lastWine.get('quantity'));

  // check external action with correct values

  this.set('model', model);
  this.set('updateStock', (model) => {
    const json = model.getProperties('name', 'quantity');
    const name = firstWine.get('name');
    assert.deepEqual(json, { name: name, quantity: firstQty-3 });
  });
  this.render(hbs`{{wine-stock wines=model sellWines=(action updateStock)}}`);

  this.$('.quantity').val(3).trigger('change');
  this.$(`option:contains("${firstWine.get('name')}")`).prop('selected', true).trigger('change');
  this.$('.button').click();

  // check values have been reset

  assert.equal(this.$('.quantity').val(), "");

  // check once again

  this.set('updateStock', (model) => {
    const json = model.getProperties('name', 'quantity');
    const name = lastWine.get('name');
    assert.deepEqual(json, { name: name, quantity: lastQty-10 });
  });

  this.$('.quantity').val(10).trigger('change');
  this.$(`option:contains("${lastWine.get('name')}")`).prop('selected', true).trigger('change');
  this.$('.button').click();

});

Ensuring the form was reset and other values update correctly.

Alternatively, actions can be tested by listening on the local action:

this.on('sellWines', (model) => {
  assert.deepEqual(...);
});

but I prefer the one demonstrated above that ensures the external action has been reached.

Integration tests, as we saw earlier, serve to check the coherence between input and output. Verifying model loading, update actions, and subsequent model loading would belong in an acceptance test.

Were you able to follow along? Any questions? Tell me in the comments below!

Enjoyed this article? Don't miss my next one!

Leave me your e-mail for content that will help you master Ember:

Do you want to master Ember fast?

Leave me your e-mail for helpful updates delivered straight to your inbox.

(A few e-mails per month. No BS. Unsubscribe anytime!)