Custom XPath Matcher for RSpec

Posted by david

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.

Comments

Leave a response