I was using RSpec on Rails to test generation of an RSS feed, and I was surprised that it did not include a built-in way to easily check XML output of a view (such as using XPath). It does have a way to check HTML output, so you can do something like the following:
1 2 3 4 5 |
response.should have_tag('ul') do with_tag('li') end |
It may be tempting to use this to do simple matches on XML output as well. Don't give into that temptation. The have_tag matcher assumes it's working against HTML, and if you're not, you may see strange behavior if any of the XML tags you have share their names with HTML tags. E.g., the "link" tag in RSS 2.0 feeds will cause strict HTML parsing to fail.
Fortunately, RSpec makes adding your own custom matchers really easy. Thanks to a couple existing tutorials, such as this one, I was able to whip up a custom XPath matcher pretty quickly:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
module Spec module Rails module Matchers class MatchXpath #:nodoc: def initialize(xpath) @xpath = xpath end def matches?(response) @response_text = response.body doc = REXML::Document.new @response_text match = REXML::XPath.match(doc, @xpath) not match.empty? end def failure_message "Did not find expected xpath #{@xpath}\n" + "Response text was #{@response_text}" end def description "match the xpath expression #{@xpath}" end end def match_xpath(xpath) MatchXpath.new(xpath) end end end end |
Where to Define Matchers
All that was left was the question of where to put the MatchXpath definition. It could go into my spec_helper file, but that's already starting to get cluttered, and I don't want this definition to be lost within a bunch of configuration code. Instead, I created a directory called "spec/matchers" and threw this definition in a file called "xpath_matches.rb" within that directory. To load up the definition, I added the following code to "spec_helper.rb":
1 2 3 4 5 6 7 |
matchers_path = File.dirname(__FILE__) + "/matchers" matchers_files = Dir.entries(matchers_path).select {|x| /\.rb\z/ =~ x} matchers_files.each do |path| require File.join(matchers_path, path) end |
Now, any matcher I define in the matchers directory will get picked up by the spec_helper file.