Ember Test Selectors

Use these light conventions to make your tests more robust.

Summary

Ember Test Selectors is a small addon that brings some conventions to your testing code. It lets you easily write and select data-test attributes in your code, giving developers an easy way to mark elements that are important for user interaction and improving the robustness of your test suite.

<h1 data-test-title> {{title}} </h1>

{{#each model as |item|}}
  {{some-component data-test-item=item.id}}
{{/each}}
import { testSelector } from 'ember-test-selectors';

...

testSelector('title')
testSelector('item', 2);

Transcript

Today I want to show you a great testing addon called Ember Test Selectors. Let's jump into the code so I can show you exactly what it does.

I'm working on this app, it's pretty simple. You can add items, give them a name, set a price, and you'll see that as I do this, the total at the bottom is being updated and showing us the total plus the tax. If we switch over to the code, you'll see that I have once acceptance test for this, and the test verifies that the total with the tax is actually correct.

The test visits the homepage, and it clicks the Add item button, inputs an item, "Fish" with a price of 13, does it again with an item of "Chips" for 6, and then verifies that the text in the total element is 21. And you can see that test being run right here.

Now, if you've written any acceptance tests like this before, you'll probably see some things that make your test suite brittle, and make your tests fail when you make very small changes to your actual Ember app. And the main thing here is the way we're selecting these elements. Now, the Ember test helpers click and fillIn take CSS selectors, and so we can put anything that is a valid CSS selector in here to find the elements we want to interact with in the way that our user would. So what we're doing is using this :contains pseudoselector to click on the button that contains the text "Add item", and this just makes it work in case we add another button, so we don't put that one instead - because what we want to verify here if we look at the app is that clicking the "Add item" button and then filling in these inputs works. And so, the whole point of this test is to make sure we're clicking and filling in the correct elements.

In fillIn we see that we use the CSS class .item-name, and we actually also use the equals pseudoselector. And this will give us the first element in the array of all elements with the .item-name class.

So if you've written tests like this, you'll probably recognize these. And test selectors like this are brittle because they're really coupled to the actual structure of the HTML within your app, which can change a lot. And so Ember Test Selectors is a way around this.


The first thing we should do is step back and ask ourselves, why are these bad selectors for our tests? Well, the first point we want to make is that item-name the class and item-price the class, those are CSS classes, and those are being used for something else within our application already - namely they're being used by developers to style these elements. And that's why they can change, even if the behavior of our application doesn't change.

The bigger point to understand here is that CSS classes already have a role within web applications. By using them to reproduce our user behavior - the way our users interact with our app - we're kind of commandeering them for another purpose, which is why our tests becomes brittle, because now CSS selectors are being used for two things instead of one. And if they change for the styling side, they can break the testing side.

So what we really want is to find something that doesn't have an existing role, that would be suitable for use within our test selectors. Now the point of a test selector is to reproduce a user behavior. And so you might say, we can't use classes because they're used for styling. What about using ids?

If we look at our app, and you open this inspector and you choose one of these components, you'll actually see ids are already taken by Ember (this component has an id of ember419). These are dynamic ids that are generated by the framework, and Ember uses them to do some bookkeeping when it comes to re-rendering templates and updating data. So in general, you should treat ids as private, and not really use them within your application code.

Another option would be to use the ARIA role attribute. This is an accessibility attribute which marks certain attributes as screenreader-friendly or other things like this; but again the problem here is that those attributes already have a role within web applications, and you can imagine them not lining up exactly right with the needs of your interface, and how you would describe the elements that are used by your end-users.

And that's really what we want. We really want a new way to identify these elements and signal to other developers that this element is for price, this element is for the name of an item and so on. Well, HTML5 added data- attributes. And data attributes are ways of adding new attributes to our HTML elements. If we open up our index template, we'll see we're using some components and some elements here. A data attribute is just something that looks like this

<div data-foo> </div>

and this is a valid HTML attribute, and what comes after data- is actually up to us. And so this lets us do something like this:

<div data-test-selector> </div>

and now we have something that's specific for us, that is just designed to let other developers and our tests know what the functionality is of this element within our interface. And so this is a great solution for avoiding all those problems with using CSS selectors, roles or ids, and it will help our test suite become way less brittle.


Now, let's start refactoring this test to use those data-test selector attributes. We'll start down here. We're looking for the element with the .items-total class to verify that our text the sum is 21. So let's find that, it's right down here - there's a <strong> element .items-total. And we'll leave this .items-total class on this element in case the application is using it for styling purposes. But we'll just come here and add a data-test attribute, and then we can call this whatever we want, so we'll call this data-test-total. Again, data-test is just a signal that this is used for our tests, and that's its only purpose. So this frees up developers to make other changes to the application without worrying about breaking the tests.

Now if we come back to the test, instead of using this selector .items-total, we'll use an attribute selector, and just look for

find('[data-test-total]')

We'll save this, come back to our test suite, and we'll see that our acceptance test passes.


At this point we could keep going, add the data-test attributes to the rest of our app, and update the rest of our tests. But I want to introduce the addon Ember Test Selectors.

This addon takes this pattern that we just described, and blesses it, and makes it conventional for your entire application. And it does a few other cool things too.

So this is called Ember Test Selectors, we can scroll down here and see it's an addon so we'll install it:

ember install ember-test-selectors

Let's come over to our server, stop it, install the addon, and then restart the server and the test suit. If we come back to the README, we'll see that the first thing it does is provide this function for us, testSelector, which just makes it a little nicer to actually find those data-test attributes within our code. And so first let's come back to our test, we'll import this at the top, and instead of actually writing this out, we'll replace this with testSelector('total').

import { testSelector } from 'ember-test-selector'

...

find(testSelector('total'));

And again this function is doing the same thing that we had before, it's just a little bit of a nicer way to write it.

Let's save this, come back to our tests, and we'll see that they pass.

OK so let's keep going and replace the rest of our selectors with data-test attributes. We'll start with the "Add item" button. So we'll go to the index, search for "Add item", and we see it's using this paper-button component. Now, one interesting thing about data attributes is that there's actually some setup required to get them working with Ember components. But because we're using this Ember Test Selectors addon, we don't have to worry about that at all.

So we can come here and add data-test-add-item as a selector to our component. And attributes to Ember components need a value, so we'll just pass in true here.

{{paper-button data-test-add-item=true}}

Now we'll come back and replace our button:contains(Add item) pseudoselector with testSelector('add-item'). We'll save these files, come back to our test suite, and we'll see that the tests pass.

Lastly, let's replace our item-name and item-price CSS selectors. We'll come to the template, and we see that we {{#each over the array of items, and then we render two inputs, one for the name and one for the price.

So let's add a data-test-item-name - and because we're using a loop here, we actually want to assign either an id or an index number here, so that from our tests we can say, fill in the first name or the second name. So we'll just use the index. We'll come to the loop up here, and add index to the arguments, and then we'll use this for our value. And we'll do the same thing with price: data-test-item-price for the first or the second item and so on.

{{#each model as |item index|}}
  {{paper-input data-test-item-name=index}}
{{/each}}

And if we save this, we can look at the app, and we can see what's happening. If we add two items, I look right here, and I can see data-test-item-name="0" and data-test-item-name="1". And so this lets us select the items more robustly in our test code.

So let's come back and rewrite these using the testSelector function. So first we'll use testSelector, we'll pass in item-name, and for the second argument to this function we'll pass in 0. And this will tell us to select the first element of this array of this loop that we're doing. And so this is how we can say, "fill in the first name", "fill in the second name" and so on.

Now you'll notice in our old test selector we actually used .item-name and then we had a space and then we had an input. And that's because in our application, when we add an item, there's actually a wrapper component that gets the class name applied to it - you'll see here .item-name - but the actual <input>, which is what the fillIn helper targets, is nested within it. And so we need to still nest that, so that the fillIn helper can actually target the input instead of that wrapping component.

Well the nice thing about testSelector is it's just going to output that string for us, and so we can just compose it with other CSS selectors already. So we'll wrap this in a string template and add that same descendent input selector like we had before. But now our test selector is more robust, because this isn't coupled to any CSS class or any implementation detail of the actual template. It's just looking for an input in any element that has the data-test-item-name attached to it.

So let's see if this works - we'll save it, come back to our test, looks good. And we'll do the same thing for price, except we'll use item-price, and we'll fill in 13. Save that, everything's working. And let's copy the same thing down here, except we're going to put in "Chips" and 6. We save that - our test is failing, so let's come back and look. We forgot to update the index here. So we wanted to choose the first item in the array (which is the second item) and put those in instead of the 0th one. So now that we've changed that we'll save it, come back, and the test is passing.

And now there's no more CSS classes in our tests. And so our test is less brittle, and it lets the designers and the developers play with the classes much more freely without worrying about breaking their test suite.

Now the addon does one more cool thing. These test selectors here that get added, well you might say this now has this data-test-add-item on it, this total has this data-test-total on it, and these are really just for developers during testing mode and maybe QA teams, and they really shouldn't end up in our production application. Well, the addon actually strips all of these tags from your production build. So once you actually deploy your app to production and run ember build --env=production, none of these tags will end up in your final output, and you'll also save a little bit of memory cost on those data-binding attributes for your actual components.


This addon is just a great example of using conventions in the Ember community, making a decision about how you're going to name your test selectors so that you and your team don't have to fight over it. The addon is getting more popular and people are used to using it, so I think this is just a shining example of something that takes more decision-making off of our plates as developers, and lets us defer to the community and focus on our applications.

Just wanted to show you that addon today, be sure you check it out and thanks for watching!

👋 Hey there, Ember dev!

We hope you enjoyed this free video 🙂

If you like it and want to keep learning with us, we've written a free 6-lesson email course about the fundamental patterns of modern component design in Ember.

To get the first lesson now, enter your best email address below:

You can also check out more details about the course by clicking here.

Questions?

Send us a tweet:

Or ask us in Inside EmberMap, our private Slack workspace for subscribers.