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 ">
Welcome to Ember,
<strong></strong>!
Welcome to Ember!
</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.
anchorStatus Quo
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.
anchorPromise chains
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.
anchorasync/await
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.
anchorchai and chai-dom
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.
anchorqunit-dom
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:
- automatically finds the correct element on the
document
(or#ember-testing
element) based on the selector passed into thedom()
function - collapses whitespace according to the HTML spec to get rid of the irrelevant
\n
part of the expected string - provides readable high level assertions for the most common checks on DOM elements
As 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/mainmatter/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.
anchorqunit-dom-codemod
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/mainmatter/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/mainmatter/qunit-dom-codemod/master/qunit-dom-codemod.js ./tests/
anchorConclusion
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.