Using rspec have_tag

Mar 31, 2008

Not everyone realises that that rspec have_tag assertion is simply a wrapper for the Test::Unit assert_select. And even if you do, do you really know how powerful a tool it is? I've seen many tests and specs where people jump through a whole heap of hoops to check that the desired element exists, well no more! Fresh for today, a quick run down on some of the most useful and powerful selectors you can use with have_tag (and by default, assert_select too).

Basic rspec have_tag usage

Alright, so the most basic of tests is to see if an element exists:

response.should have_tag("li")

Here we aren't fussy, so long as any <li> element exists the assertion will pass. We can be a little more specific by also requiring that <li> tag to contain certain text:

response.should have_tag("li", :text => "I'm a list item")

Now it will match `<em><li>I'm a list item</li></em>`. And taking baby steps again, we can ensure that the list item has been given a specific class, ID, or both with one of the following:

response.should have_tag("li.my_class", :text => "I'm a list item")
response.should have_tag("li#my_element_id", :text => "I'm a list item")
response.should have_tag("li#my_element_id.my_class", :text => "I'm a list item")

You can essentially reference the elements exactly as you would in a CSS file.

Matching element attributes with have_tag

What if you want to ensure that something other than a class or ID exists on the element? Well again just like in CSS have_tag in rspec will let you target elements containing attributes:

response.should have_tag("img[alt=My accessible text]")

This will check that a tag like `<em><img alt="My accessible text"></em>` exists somewhere in the result rendered by you view. You can extend this further with wildcard and pattern matchers on the attributes

response.should have_tag("img[alt~=readable accessible usable]") # Match any of these words
response.should have_tag("img[alt^=My]") # Match attribute beginning with "My"
response.should have_tag("img[alt$=text]") # Match attribute ending with "text"
response.should have_tag("img[alt*=essibl]") # Match "essibl" anywhere in attribute
response.should have_tag("img[alt]") # Match any img element with alt attribute

Advanced rspec have_tag CSS selectors

Taking it up a notch are some of the more advanced CSS selectors. In reality, I rarely (if ever) need to use these, but they're handy to have for those tricky situations where you'd otherwise have to construct a loop to go through all the element manually. Assume we have a list of elements like this:

  &lt;li&gt;I'm element 1&lt;/li&gt;
  &lt;li&gt;I'm element 2&lt;/li&gt;
  &lt;li&gt;I'm element 3&lt;/li&gt;
  &lt;li&gt;I'm element 4&lt;/li&gt;
  &lt;li&gt;I'm element 5&lt;/li&gt;

We could use the following assertions to get the respective elements based on their order:

response.should have_tag("li:nth-child(2)", :text => "I'm element 2")
response.should have_tag("li:nth-last-child(2)", :text => "I'm element 4")
response.should have_tag("ul:first-child", :text => "I'm element 1")
response.should have_tag("ul:last-child", :text => "I'm element 5")

You can take a similar approach to retrieve elements based on their type. Consider the following chunk of HTML:

  &lt;h1&gt;My big heading!&lt;/h1&gt;
  &lt;p&gt;I've got some paragraph text&lt;/p&gt;
  &lt;h2&gt;My smaller heading...&lt;/h2&gt;
  &lt;p&gt;I'm going to describe the list below&lt;/p&gt;
    &lt;li&gt;I'm list 1&lt;/li&gt;
    &lt;li&gt;I'm list 2&lt;/li&gt;
    &lt;li&gt;I'm list 3&lt;/li&gt;

and then the have_tag selectors to get those elements:

response.should have_tag("p:nth-of-type(2)", :text => "I'm going to describe the list below")
response.should have_tag("li:nth-last-of-type(2)", :text => "I'm list 2")
response.should have_tag("p:first-of-type", :text => "I've got some paragraph text")
response.should have_tag("p:last-of-type", :text => "I'm going to describe the list below")

Using formulas in nth- specified selectors

As you see, we can target an element that is a specific child in the order of the DOM hierarchy. What about testing the following:

  &lt;li class="highlight"&gt;I'm element 1&lt;/li&gt;
  &lt;li&gt;I'm element 2&lt;/li&gt;
  &lt;li&gt;I'm element 3&lt;/li&gt;
  &lt;li class="highlight"&gt;I'm element 4&lt;/li&gt;
  &lt;li&gt;I'm element 5&lt;/li&gt;
  &lt;li&gt;I'm element 6&lt;/li&gt;
  &lt;li class="highlight"&gt;I'm element 7&lt;/li&gt;
  &lt;li&gt;I'm element 8&lt;/li&gt;

How would we test that all the appropriate elements have had their additional class applied? Well the easiest way would be to provide a formula to the _`nth-`_ selector types.

response.should have_tag("li:nth-of-type(3n+1).highlight", :count => 3)

That will ensure that every third `<li>` element, starting at number 1, will have the highlight class applied and that we return exactly 3 matches.

Asserting the expected number of matches with rspec have_tag

As you saw in the previous example, it's quite simple to specify exactly how many matches should be returned. But what if you don't know? (note: I can't think of a valid reason why you wouldn't if you are mocking out your tests correctly, but I digress) Then you can specify an upper or lower limit or a range:

response.should have_tag("li:nth-of-type(3n+1).highlight", :maximum => 4)
response.should have_tag("li:nth-of-type(3n+1).highlight", :minimum => 1)
response.should have_tag("li:nth-of-type(3n+1).highlight", 1..4)

Iterating over child elements in a block

Now imagine you have a page with more than one form on it. One for posting comments to a blog, one for logging in maybe? We want to test that the login form exists (and is going to the correct path), and that there are input elements for username and password within that form. Check this out:

response.should have_tag "form[action=/sessions]" do
  with_tag "input[type=text][name=username]"
  with_tag "input[type=password][name=password]"

Neat eh?

Hi, I'm Glenn! 👋 I'm currently Director of Product @ HashiCorp, and we're hiring! If you'd like to come and work with me and help make Terraform Cloud even more amazing we have multiple positions opening in Product ManagementDesign, and Engineering & Engineering Management across a range of levels (i.e., junior through to senior). Please send in an application ASAP so we can get in touch.