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 moreassert_select
s and for which the rootassert_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_select
s 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 div
s on the page each containing two div
s. 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 div
s 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 entireassert_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!).