Making your UI tests resilient to change

October 23, 2017 0 Comments

Making your UI tests resilient to change

 

 

Photo by Warren Wong on Unsplash

User interface tests are famously finicky and prone to breakage. Let’s talk about how to improve this.

NOTE: This is a cross-post from my newsletter. I publish each email two weeks after it’s sent. Subscribe to get more content like this earlier right in your inbox! 💌

You’re a developer and you want to avoid shipping a broken login experience, so you’re writing some tests to make sure you don’t. Let’s get a quick look at an example of such a form:

Login form from the Conduit App
const form = (
<form onSubmit={this.submitForm}>
<fieldset>
<fieldset className="form-group">
<input
className="email-field form-control form-control-lg"
type="email"
placeholder="Email"
/>
</fieldset>
<fieldset className="form-group">
<input
className="password-field form-control form-control-lg"
type="password"
placeholder="Password"
/>
</fieldset>
<button
className="btn btn-lg btn-primary pull-xs-right"
type="submit"
disabled={this.props.inProgress}
>
Sign in
</button>
</fieldset>
</form>
)

Now, if we were to test this form, we’d want to fill in the username, password, and submit the form. To do that properly, we’d need to render the form and query the document to find and operate on those nodes. Here’s what you might try to do to make that happen:

const emailField = rootNode.querySelector('.email-field')
const passwordField = rootNode.querySelector('.password-field')
const submitButton = rootNode.querySelector('.btn')

And here’s where the problem comes in. What happens when we add another button? What if we added a “Sign up” button before the “Sign in” button?

<button
className="btn btn-lg btn-secondary pull-xs-right"
disabled={this.props.inProgress}
>
Sign up
</button>
<button
className="btn btn-lg btn-primary pull-xs-right"
type="submit"
disabled={this.props.inProgress}
>
Sign in
</button>

Whelp, that’s going to break our tests. Total bummer.

total bummer…

But that’d be pretty easy to fix right?

// change this:
const submitButton = rootNode.querySelector('.btn')
// to this:
const submitButton = rootNode.querySelectorAll('.btn')[1]

And we’re good to go! Well, if we start using CSS-in-JS to style our form and no longer need the email-field and password-field class names, should we remove those? Or do we keep them because our tests use them? Hmmmmmmm..... 🤔

What I don’t like about using class names for my selectors is that normally we think of class names as a way to style things. So when we start adding a bunch of class names that are not for that purpose it makes it even harder to know what those class names are for and when we can remove class names.

And if we simply try to reuse class names that we’re already just using for styling then we run into issues like the button up above. And any time you have to change your tests when you refactor or add a feature, that’s an indication of a brittle test. The core issue is that the relationship between the test and the source code is too implicit. We can overcome this issue if we make that relationship more explicit.

If we could add some metadata to the element we’re trying to select that would solve the problem. Well guess what! There’s actually an existing API for this! It’s data- attributes!

Data from “Star Trek: The Next Generation” saying “YES!”

So let’s update our form to use data- attributes:

const form = (
<form onSubmit={this.submitForm}>
<fieldset>
<fieldset className="form-group">
<input
className="form-control form-control-lg"
type="email"
placeholder="Email"
data-testid="email"
/>
</fieldset>
<fieldset className="form-group">
<input
className="form-control form-control-lg"
type="password"
placeholder="Password"
data-testid="password"
/>
</fieldset>
<button
className="btn btn-lg btn-primary pull-xs-right"
type="submit"
disabled={this.props.inProgress}
data-testid="submit"
>
Sign in
</button>
</fieldset>
</form>
)

And now, with those attributes, our selectors look like this:

const emailField = rootNode.querySelector('[data-testid="email"]')
const passwordField = rootNode.querySelector('[data-testid="password"]')
const submitButton = rootNode.querySelector('[data-testid="submit"]')

Awesome! So now, no matter how we change our markup, as long as we keep those data-testid attributes intact, then our tests wont break. Plus, it's much more clear what the purpose of these attributes is which makes our code more maintainable as well.

Here’s a little utility called sel (short for select) that I use sometimes to make this a little easier:

const sel = id => [data-testid=&quot;${id}&quot;]
const emailField = rootNode.querySelector(sel('email'))
const passwordField = rootNode.querySelector(sel('password'))
const submitButton = rootNode.querySelector(sel('submit'))

This is great for end to end tests as well. So I suggest that you use it for that too! However, some folks have expressed to me concern about shipping these attributes to production. If that’s you, please really consider whether it’s actually a problem for you (because honestly it’s probably not as big a deal as you think it is). If you really want to, you can transpile those attributes away with babel-plugin-react-remove-properties.

I should also note that if you’re using enzyme to test React components, you might be interested in this to avoid some issues with enzyme’s find returning component instances along with DOM nodes.

I hope this is helpful to you. Good luck! Enjoy :)

Things to not miss:

P.S. If you like this, make sure to subscribe, follow me on twitter, buy me lunch, and share this with your friends 😀 And feel free to retweet this:

P.S.P.S. I publish all of these newsletters after two weeks to my Medium publication. You can find them here: blog.kentcdodds.com 📝

“Why don’t you use id attributes instead?”

The problem with using id is you shouldn’t include more than one of the same id on a single page which could definitely happen if you’re trying to use it for something like this. It also is less explicit about the purpose than data-testid is. So I still prefer data-testid :)

“What about selecting based on content?”

Just ask my friend Kyle Shevlin 😅

So yeah… Don’t try to select for something that’s not intended for testing…

“What about a convention for the class name? Like all ft- prefixed class names are ‘for tests’?”

I think that’s fine. Except I’m not huge on conventions where they can be avoided. There’s something nice about data-testid attributes for me. Data attributes can literally be used for anything, and using test in the name of the attribute automatically makes it clear what the purpose of this thing is. Whereas I have to learn what ft means, and it’s less obvious so it could easily be forgotten when it’s no longer needed.

“What if I’m iterating over a list of items that I want to put the data-testid="item" attribute on. How do I distinguish them from each other?”

Whelp, first off you can make your selector just choose the one you want by including :nth-child(4) in the selector. Or you could include the index or an ID in your attribute: data-testid={item-${item.id}}. I’ve done this and it works great :)


Tag cloud