As an introduction to what this means let's start with an example template for an Ember app that we will write a test for:
<h1 class="title {{if username "has-username"}}">
{{#if username}}
Welcome to Ember,
<strong>{{username}}</strong>!
{{else}}
Welcome to Ember!
{{/if}}
</h1>
From the template above you can see that we have essentially two states that we
need to test: one with and one without a username
property being set.
An acceptance test for such a template could look roughly like this:
test('frontpage should be welcoming', function (assert) {
visit('/');
andThen(function () {
assert.equal(find('h1.title').textContent.trim(), 'Welcome to Ember!');
assert.notOk(find('h1.title').classList.contains('has-username'));
});
fillIn('input.username', 'John Doe');
andThen(function () {
assert.equal(
find('h1.title').textContent.trim(),
'Welcome to Ember,\n John Doe!',
);
assert.ok(find('h1.title').classList.contains('has-username'));
});
});
First we will visit
the index page andThen
check if the welcome message
matches our expectation. Next we will fill
an <input>
field with a custom
username
like "John Doe" andThen
finally we will check if the welcome
message was updated correctly.
If you've used Ember.js for some time you will probably
be used to the andThen
blocks above. One of the reasons for them to exist is
that Ember.js is older than the Promise
implementation that came with ES6 and
before that existed there was already a need for handling async behavior in
tests.
Since Ember.js has kept up very well with the latest developments in JavaScript
you can also use a Promise
chain instead of the andThen
blocks which would
look like this:
test('frontpage should be welcoming', function (assert) {
return visit('/')
.then(function () {
assert.equal(find('h1.title').textContent.trim(), 'Welcome to Ember!');
assert.notOk(find('h1.title').classList.contains('has-username'));
return fillIn('input.username', 'John Doe');
})
.then(function () {
assert.equal(
find('h1.title').textContent.trim(),
'Welcome to Ember,\n John Doe!',
);
assert.ok(find('h1.title').classList.contains('has-username'));
});
});
While that code makes it more obvious that we are dealing with asynchronous code
here, it also make the code a little harder to read. Which is one of the reasons
why a lot of Ember developers still prefer the andThen
blocks over using
Promise
chains.
In one of the recent changes to the JavaScript language (or ECMAScript to be
precise) two new keywords were introduced to simplify dealing with Promises
:
async
and await
.
Whenever you mark a function
as async
it will automatically return a
Promise
and once you return
something from that function it will resolve
that Promise
. Similarly if you throw
an error it will reject
the
Promise
.
But the real power comes with the await
keyword that can only be used inside
of async
functions. Using await
you can wait on another Promise
before
resolving or rejecting the Promise
of your own async
function.
That description was pretty abstract so let's look at an example using the
Promise
chain above:
test('frontpage should be welcoming', async function (assert) {
await visit('/');
assert.equal(find('h1.title').textContent.trim(), 'Welcome to Ember!');
assert.notOk(find('h1.title').classList.contains('has-username'));
await fillIn('input.username', 'John Doe');
assert.equal(
find('h1.title').textContent.trim(),
'Welcome to Ember,\n John Doe!',
);
assert.ok(find('h1.title').classList.contains('has-username'));
});
As you can see this looks a lot more readable than what we had before and almost like the synchronous code we usually write.
The assertions (the lines starting with assert.
) however are still quite hard
to read, and it takes a short while to figure out what the intent of that
assertion was.
If you're using Mocha and Chai to write your tests, you are already used to more readable assertions since Chai emphasizes an "expressive language and readable style" for their assertions.
Fortunately for Chai there is a plugin called
chai-dom
which provides even
better assertions so that we could rewrite our assertions above to something
like:
expect(find('h1.title')).to.have.text('Welcome to Ember!');
expect(find('h1.title')).to.have.class('has-username');
// ...
expect(find('h1.title')).to.have.text('Welcome to Ember,\n John Doe!');
expect(find('h1.title')).to.have.class('has-username');
chai-dom
is also supported by default in ember-cli-chai
, so if you used
ember-cli-chai
today you only need to npm install --save-dev chai-dom
,
restart Ember CLI and now you can use the additional assertions that chai-dom
provides.
While ember-cli-chai
also works with QUnit it is essentially just a hack and
not really supported properly by QUnit or Chai so be careful if you're using it.
As we were getting more and more annoyed by the hard-to-read assertions when
using QUnit we were starting to wonder if it would be possible to build
something like chai-dom
but for QUnit instead and how that would look like.
After a bit of brainstorming we figured we would want our assertions to look
roughly like this:
assert.dom('h1.title').hasText('Welcome to Ember!');
assert.dom('h1.title').doesNotHaveClass('has-username');
// ...
assert.dom('h1.title').hasText('Welcome to Ember, John Doe!');
assert.dom('h1.title').hasClass('has-username');
Compared to what we started with this:
document
(or #ember-testing
element) based on the selector passed into the dom()
function\n
part of the expected stringAs you might have figured out by now we've not just planned how it could look, we've also built and released it at https://github.com/simplabs/qunit-dom.
One additional advantage for Ember.js users is that it automatically hooks
itself into the build pipeline of your projects, so all you need to do is
ember install qunit-dom
, and then you can immediately start using it!
You can find examples of what assertions are available in the README of the project and even more information in the API reference.
During the EmberFest conference we realized that while a lot of people would
probably appreciate what we had built, nobody would go over their thousands of
existing assertions and rewrite them all to use qunit-dom
. Since a lot of the
existing assertions in our client projects followed similar patterns we figured
it might be possible to build a
codemod
that did most of the rewriting automatically for us.
After that initial thought we started working and after only a few minutes we already had a working proof-of-concept including passing tests. Since then we have put in some more work into the codemod and are happy to share it with you at https://github.com/simplabs/qunit-dom-codemod.
All you need to do is install jscodeshift
(the thing that runs the codemod):
npm install -g jscodeshift
and then run the codemod e.g. on your tests
folder:
jscodeshift -t https://raw.githubusercontent.com/simplabs/qunit-dom-codemod/master/qunit-dom-codemod.js ./tests/
Moving the tests to async/await
and qunit-dom
makes them a lot more readable
and easier to understand for new developers and is just a few keystrokes away if
you're already using Ember.js for your frontend projects. If you need help
refactoring your tests or even your production code to be more structured and
understandable feel free to contact us.