Testing for HTML Tags in Rails Plugins

Brian Landau, Former Developer

Article Category: #Code

Posted on

When creating Rails plugins that add ActionView helpers, we often test to ensure they produce specific tags with specific attributes. When testing this type of assertion within controller tests, we have the very useful assert_tag and assert_select; but in plugin tests and elsewhere, these aren't available. Adding this functionality to your own tests turns out to be somewhat convoluted.

The first realization you come to is that assert_tag and assert_select can't be used because they test against the response body of a controller. Since we're trying to test functionality independent of other components, using ActionController when it's not necessary is not recommended. You might be tempted at this point to just test against a regular expression and forget using these methods. That is doable, but a little unmanageable in the long run. First off, attributes can't be expected to be in a specific order, and their order doesn't matter, so you don't want to test for that. This will even initially make your regexes fairly complicated. Here's an example of testing for a specific name attribute:

 def test_for_name_attribute tag = text_field_tag('login') assert_match /<input\s+(([\w^(name)]+="([^"'><]+)?"\s+)+)?name="login"((\s+[\w^(name)]+="([^"'><]+)?")+)?(\s+)?\/>/, tag end 

As you can see, even matching one attribute can result in a extremely long and ugly regex. What if we want to match multiple attributes and nested tags? This quickly becomes untenable.

If you look to include the component used by assert_tag to find and match specific tags, you will find require 'html/document'. Try just requiring this by doing this at the top of your test:

 require 'test/unit' require 'rubygems' require 'active_support' require 'action_view' require 'html/document' 

You'll find you get a no such file to load -- html/document (MissingSourceFile) error. If you dig around in the Rails code, you'll find it lives in action_controller/vendor/html-scanner. To be able to include html/document, you need to first include action_controller.

Now that we have html/document included, let's make a test helper method to match a tag specification. This takes little change from the original assert_tag method.

 def assert_tag_in(*opts) target = HTML::Document.new(opts.shift, false, false) opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first assert !target.find(opts).nil?, "expected tag, but no tag found matching #{opts.inspect} in:\n#{target.inspect}" end 

With this, you can now use assert_tag_in to do the same test as above:

 def test_for_name_attribute tag = text_field_tag('login') assert_tag_in tag, :input, :attributes => {:type => 'text', :name => 'login'} end 

The whole code for a sample test_helper.rb file for plugins can be found in this pastie.
Also, look at the documentation for assert_tag for all the options available.

From this, an assert_no_tag_in could easily be made. You could use a similar technique to make a assert_select_in, although assert_select is much more complex and often overkill for simple helpers where you aren't testing as large a number of tags or as complicated as nesting.

Update:

Given the interest in a hpricot version of the assert_tag_in, I've put together one:

 def assert_tag_in(target, match) target = Hpricot(target) assert !target.search(match).empty?, "expected tag, but no tag found matching #{match.inspect} in:\n#{target.inspect}" end 

With this you can then write a similar test like this:

 def test_for_name_attribute tag = text_field_tag('login') assert_tag_in tag, 'input[@name="login"]' end 

The alternative test_helper.rb using hpricot can be found in this pastie.

Related Articles