A better assert_select (assert_elements)

I've been using Rails since the days of assert_tag (an abomination that required far too much work to do very must testing for specific tags), which was powerful enough to take a look at the responses to gets and posts and make sure that they fit basic criteria.

Unfortunately, its syntax was far too verbose. Here's an actual example from the Rails API:

assert_tag(
  tag: "div",
  ancestor: { tag: "ul" },
  parent: { tag: "li", attributes: { class: "enum" } }
  descendant: { tag: "span", child: /hello world/ })

As you can see, this is no fun, so I was really happy to see assert_select come into the picture, first as a plugin, and finally getting into the Rails core in Rails 1.2. Here's what it does:

  • You can do assert_select "css_selector"
  • You can assert that there's an exact number of matches, a minimum number, or a maximum number
  • You can narrow the set of matched elements by a specific text string (or match a RegExp)
  • You can pass assert_select a block, which contains more assert_selects and for which the root assert_select only returns true if the asserts inside pass for the collection of elements matched in the initial one.

It's pretty cool, but it also has a two major drawbacks:

First, it makes a fundamentally wrong assertion about how it handles blocks. It tests all assert_selects in the block against the collection of elements matched by the initial selection. What that means is that if you want to test that at least one div on the page contains exactly two divs inside it, the basic block technique will fail:

assert_select "div" do
  assert_select "div", :count => 2
end

This will fail if you have two divs on the page each containing two divs. That's because the inner assert is being run against the set of all elements matched by the first assert_select "div", which will find four elements (two divs for each matched div).

What you want instead is for assert_select to work if and only if the inner asserts pass for at least one of the matched divs.

The second drawback is that the plugin uses its own home-grown selector system, which is both slower than hpricot and, more importantly, more limited than hpricot. It uses CSS2 selectors, and has no support for things like ancestor selection (like the original assert_tag has).

Hpricot, on the other hand, supports full CSS3 as well as some basic XPath (pretty much the same stuff that jQuery supports; it seems to have started off as a Ruby port of jQuery's selector engine).

Thankfully, Luke Redpath already did the initial work to make it possible to test Rails views with hpricot. He made it possible to do stuff like (his examples):

  • assert_equal "My Funky Website", tag('title')
  • assert_equal 20, tags('div.boxout').size
  • assert_equal 'visible', element('div#site_container').attributes['class']

That's really cool, and I was strongly considering leaving it at that and using Luke's plugin, but there were some really cool things you could do with assert_select that you cannot do as easily with Luke's plugin.

So I built on Luke's plugin to create a new assert, called assert_elements:

  • It has identical syntax to assert_select
    • You can use full CSS3 and limited XPath syntax (hello again to ancestor support)
    • If you pass an assert_elements a block, it tests the results on each matched elements, and if all of the asserts return true at least once, the entire assert_elements passes.
  • If you pass a block, and have count: 0, it'll pass if there is no element in the set of matched elements for which all of the child asserts pass.
    • It is fast.

Known Issues:

  • This is my first release of assert_elements to the world. There will probably be issues I have not yet resolved. Please email me at wycats AT gmail DOT com with any bug reports.
    • The error messages are obtuse, especially with blocks.
    • As with Luke's original plugin, there are no tests. I have to figure how to test an assert plugin.
    • Hpricot 0.4 has a serious issue with certain selectors (including [@att*=val]). The issue is no longer present in hpricot's most recent candidate build (hpricot 0.4.99) so install it via:
$ gem install hpricot --source http://code.whytheluckystiff.net

You can get assert_elements as a plugin at http://tools.assembla.com/svn/assert_elements/trunk/assert_elements/ by doing:

$ script/plugin install http://tools.assembla.com/svn/assert_elements/trunk/assert_elements/

Very many thanks to Luke Redpath. This plugin build very much on the work he did (in fact, most of the source excepting the assert_elements method itself and some minor changes to support blocks is Luke's!).