Decoupling feature specs from markupSimple guidelines for resilient tests
Typical scenario
Consider this HTML fragment:
<section id="recipes">
<h1>The tasty recipes of uncle Alfred <button class="close">Close</button></h1>
<div id="toad-recipe" class="recipe">
<h2>Toad lasagna <button class="add">Add to favorites</button></h2>
<p>First, you have to catch a big, juicy toad.</p>
</div>
</section>
```html
And this css:
```css
section#recipes button.close{
/* some funky styles */
}
And javascript (jQuery):
$("#recipes > h1 > button.close").click(function(){
/* some funky behavior */
});
And this example step definition in your tests:
click_on("div#toad-recipe > h2 > button.add")
# some funky testing
When it goes bad
Now, suppose that you want to change the design of your recipes page, you’ll be very likely to break some javascript functionality or your test. Can you spot the problems generated by the example above ?
Using the same attribute(s) for different concerns (using the class for styles, javascript and testing) will make you lose time and energy each time you’ll want to change one of these, it makes your code rigid.
In addition to that, all of these concerns are way too much dependent on the HTML structure. Should you want to change the h2 for an h3, and you’ll have to fix everything everywhere.
The solution: a clear separation of concerns
Here are my guidelines for manageable html, css and javascript in a well-tested project, I’m still experimenting with this and I’m open to any suggestion or critic.
- For css: use class attributes only
- For javascript: use IDs for unique elements or data-attributes for generic elements
- For testing: use data-attributes only, except when you’re targeting a specific object from the domain, in which case you’re allowed to use the ID.
The refactored HTML fragment:
<section id="recipes" class="recipes" data-purpose="recipes-list">
<h1>
The tasty recipes of uncle Alfred
<button class="close" data-purpose="close-button">Close</button>
</h1>
<div id="toad-recipe" class="recipe" data-purpose="entry">
<h2>
Toad lasagna
<button class="add" data-purpose="add-button">Add to favorites</button>
</h2>
<p>First, you have to catch a big, juicy toad.</p>
</div>
</section>
And css:
.recipe .add {
/* some funky styles */
}
$("[data-purpose='add-button']").click(function(){
// some funky javascript
});
And step definition:
within( "[data-purpose='recipes-list']" )
click_on( "[data-purpose='add-button']" )
end
# some funky testing
Benefit
With this approach, the different concerns (js, css, testing) are decoupled from each other.
By reserving the usage of css classes for styling only, the ids for javascript and the data-attributes for javascript & testing, it means that you can completely redesign/refactor/rework one of these concern without affecting the others.
On a large project, it’s a life saver.
Some more tips
For css:
- You should really never use ids to style elements, find a way to tag the specific element with a meaningful class instead.
For javascript:
- Instead of writing js code for specific cases, write generic code and use data-attributes instead, like Twitter Bootstrap does.
- Use ids only to target a unique element corresponding to real objects in the domain logic, ex: #post51.
For tests:
- Use data attributes instead of classes to describe the purpose of elements
- With links: use the rel attribute to describe its purpose, as described by Steve Klabnik on his blog:
<a href="/articles/1/edit" rel="edit-article">Edit this article</a>
When /^I choose to edit the article$/ do
find("//a[@rel='edit-article']").click
end