Updated 2019-01-25: updated to use ember-mocha
directly.
Here’s a walkthrough of setting up my preferred Ember testing stack, getting all the following tools to play nicely together:
- New-style Ember tests with async/await and no jQuery
- Mocha and Chai, with chai-dom for nicer assertions
- Sinon for test doubles, with sinon-chai for nicer assertions
- Mirage for mocking the backend
To start out, since Mocha will take the place of QUnit, we want to remove QUnit-related packages first:
$ yarn remove ember-cli-qunit ember-qunit qunit-dom
(You might only have ember-cli-qunit
or only ember-qunit
; the above command should cover either case.)
We also want to remove some QUnit-specific code from our tests/test-helper.js
file:
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();
Next, we install the ember-mocha
addon:
$ ember install ember-mocha
We also want to add ember-cli-chai
to get access to Chai, an assertion framework:
$ ember install ember-cli-chai
Now Mocha and Chai should be ready. To see them in action, let’s generate a component test:
$ ember generate component my-component
You’ll see the following test generated:
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { setupRenderingTest } from 'ember-mocha';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
describe('Integration | Component | my-component', function() {
setupRenderingTest();
it('renders', async function() {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`{{my-component}}`);
expect(this.element.textContent.trim()).to.equal('');
// Template block usage:
await render(hbs`
{{#my-component}}
template block text
{{/my-component}}
`);
expect(this.element.textContent.trim()).to.equal('template block text');
});
});
This is using Mocha and Chai, and it passes.
Let’s customize our component and add a bit more substantial of an assertion to test it. First, let’s just drop a “Hello, world!” message into the component’s template:
<div class="welcome">Hello, world!</div>
Now, make the following changes to the component test:
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { setupRenderingTest } from 'ember-mocha';
-import { render } from '@ember/test-helpers';
+import { render, find } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
describe('Integration | Component | my-component', function() {
setupRenderingTest();
it('renders', async function() {
- // Set any properties with this.set('myProperty', 'value');
- // Handle any actions with this.set('myAction', function(val) { ... });
-
await render(hbs`{{my-component}}`);
-
- expect(this.element.textContent.trim()).to.equal('');
-
- // Template block usage:
- await render(hbs`
- {{#my-component}}
- template block text
- {{/my-component}}
- `);
-
- expect(this.element.textContent.trim()).to.equal('template block text');
+ expect(find('.welcome').textContent).to.include('Hello, world!');
});
});
Run the tests with ember test
and they should pass.
When using Chai with rendering tests, there’s an even more readable way to make assertions: chai-dom
. Let’s install it:
$ yarn add --dev chai-dom
Now we can make the following change to our assertion:
await render(hbs`{{my-component}}`);
- expect(find('.welcome').textContent).to.include('Hello, world!');
+ expect(find('.welcome')).to.contain.text('Hello, world!');
});
To me, “expect to contain text” seems more readable than “expect its text content to include”.
Next let’s add Sinon, the popular test double library. It allows you to create functions that stand in for real functions in your tests, for purposes like asserting that a function was called. This is great for testing the “data down, actions up” approach in your components.
Add the Sinon addon:
$ ember install ember-sinon
Now let’s add an action to our component:
import Component from '@ember/component';
export default Component.extend({
+ actions: {
+ handleClick() {
+ this.onClick();
+ }
+ }
});
Add a button to the component’s template to trigger the action:
<button onclick={{action "handleClick"}}>Do Action</button>
When the button is clicked, we call the component’s handleClick
action, which calls a passed-in onClick
function. How can we test this? Add the following to the test file:
import { setupRenderingTest } from 'ember-mocha';
-import { render, find } from '@ember/test-helpers';
+import { render, find, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
+import sinon from 'sinon';
...
await render(hbs`{{my-component}}`);
expect(find('.welcome')).to.contain.text('Hello, world!');
});
+
+ it('calls a passed-in action', async function() {
+ const handleClick = sinon.spy();
+ this.set('handleClick', handleClick);
+
+ await render(hbs`{{my-component onClick=handleClick}}`);
+ await click('button');
+
+ expect(handleClick.called).to.be.true;
+ });
});
We call sinon.spy()
to create a spy function, then we pass it in to the component as an argument. We click the button, then we assert that handleClick
’s called
property was set to true. As you might expect, Sinon sets it to true when it’s called.
We can do even better on the assertions for Sinon, too, though. Add sinon-chai
:
$ yarn add --dev sinon-chai
Now change the assertion like so:
await click('button');
- expect(handleClick.called).to.be.true;
+ expect(handleClick).to.have.been.called;
});
To me, “expect handleClick to have been called” is more readable than “expect handleClick called to be true”.
There’s one last addon that no Ember testing setup can do without: Mirage. It provides a fake web service layer allowing you to set up backend requests and responses in tests, or even in development mode if your backend is not yet ready. Let’s install it:
$ ember install ember-cli-mirage
Let’s set up a simple model so we can test it with Mirage:
$ ember generate model widget
Add a name
attribute to the widget:
import DS from 'ember-data';
export default DS.Model.extend({
+ name: DS.attr(),
});
Generate an index route:
$ ember generate route index
In app/routes/index.js
, load the widgets in the model hook:
import Route from '@ember/routing/route';
export default Route.extend({
+ model() {
+ return this.store.findAll('widget');
+ },
});
And replace the route’s template app/templates/index.hbs
with this:
<ul class="widgets">
{{#each model as |widget|}}
<li class="widget">{{widget.name}}</li>
{{/each}}
</ul>
Next, generate an acceptance test:
$ ember generate acceptance-test mirage
To set up the test to work with Mirage, make the following changes:
import { describe, it } from 'mocha';
import { expect } from 'chai';
import { setupApplicationTest } from 'ember-mocha';
-import { visit, currentURL } from '@ember/test-helpers';
+import { visit, find } from '@ember/test-helpers';
+import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
describe('Acceptance | mirage', function() {
- setupApplicationTest();
+ let hooks = setupApplicationTest();
+ setupMirage(hooks);
Notice how we save the return value of setupApplicationTest()
to a variable, then pass it as an argument to setupMirage()
.
Then replace the existing it()
with the following:
it('allows creating models', async function() {
let widget = server.create('widget', {
name: 'Awesome Widget',
});
await visit('/');
let widgets = find('.widget');
expect(widgets).to.contain.text(widget.name);
});
Finally, set up the Mirage route to serve the widget endpoint. Add the following to mirage/config.js
:
export default function() {
+ this.get('/widgets');
}
Run ember test
again and your acceptance test should pass.
With that, all our testing tools are set up and working together: Mocha, Chai, Sinon, Mirage, and Ember’s new testing APIs. Give them a try and see what you think!