XPath matchers for rspec
Posted by Jim Morris on Wed Jan 02 13:32:50 -0800 2008
I've been working on a project that is mostly Java for the last many months, so haven't had much Ruby or Rails stuff to share.
However one thing I found when working on my tests in Java was an xpath matcher for JUnit 4.0 using the Hamcrest libraries.
When I dropped back into Ruby to write some cron scripts that process information from the database and generate xml files I wanted to check the script with an rspec, and check the xml files it was generating.
To do this I wanted to use a similar matcher to the Hamcrest ones, but use it in RSpec.
I Googled around and found a simple example, but it wasn't very sophisticated and didn't check what I needed so I upgraded it to do the kind of matches I needed, the results are here.
require 'rexml/document' require 'rexml/element' module Spec module Matchers # check if the xpath exists one or more times class HaveXpath def initialize(xpath) @xpath = xpath end def matches?(response) @response = response doc = response.is_a?(REXML::Document) ? response : REXML::Document.new(@response) match = REXML::XPath.match(doc, @xpath) not match.empty? end def failure_message "Did not find expected xpath #{@xpath}" end def negative_failure_message "Did find unexpected xpath #{@xpath}" end def description "match the xpath expression #{@xpath}" end end def have_xpath(xpath) HaveXpath.new(xpath) end # check if the xpath has the specified value # value is a string and there must be a single result to match its # equality against class MatchXpath def initialize(xpath, val) @xpath = xpath @val= val end def matches?(response) @response = response doc = response.is_a?(REXML::Document) ? response : REXML::Document.new(@response) ok= true REXML::XPath.each(doc, @xpath) do |e| @actual_val= case e when REXML::Attribute e.to_s when REXML::Element e.text else e.to_s end return false unless @val == @actual_val end return ok end def failure_message "The xpath #{@xpath} did not have the value '#{@val}' It was '#{@actual_val}'" end def description "match the xpath expression #{@xpath} with #{@val}" end end def match_xpath(xpath, val) MatchXpath.new(xpath, val) end # checks if the given xpath occurs num times class HaveNodes #:nodoc: def initialize(xpath, num) @xpath= xpath @num = num end def matches?(response) @response = response doc = response.is_a?(REXML::Document) ? response : REXML::Document.new(@response) match = REXML::XPath.match(doc, @xpath) @num_found= match.size @num_found == @num end def failure_message "Did not find expected number of nodes #{@num} in xpath #{@xpath} Found #{@num_found}" end def description "match the number of nodes #{@num}" end end def have_nodes(xpath, num) HaveNodes.new(xpath, num) end end end
The first matcher HaveXPath was pretty much the original I found on
the net, it simply checks that an XPath exists, I don't use this one.
The next matcher MatchXPath is more like what I was using in Java,
it gets an element from the xpath and checks the value is equal to the
one expected string. I will eventually add regex matching and arrays
of strings or regexs to check against.
The last one HaveNodes I find handy to make sure a given xpath matches the
expected number of nodes.
I even wrote an rspec to check the matchers, and this also is handy to show the way to use them.
Note that you can pass the matchers a String containing the XML or (much faster) a REXML::Document.
require 'matchers' describe "test matchers" do before(:each) do @xml= <<-EOFXML <?xml version='1.0'?> <claims> <testnode1> <day> <rank order='1' value='0' userid='26' alias='user25'/> <rank order='2' value='0' userid='93' alias='user92'/> <rank order='3' value='0' userid='55' alias='user54'/> <sometext>this is text</sometext> </day> </testnode1> </claims> EOFXML @doc= REXML::Document.new(@xml) end it "should test xpath" do @xml.should have_nodes("/claims/*", 1) @doc.should have_nodes("/claims/*", 1) @doc.should have_nodes("/claims/testnode1/day/rank", 3) @doc.should have_xpath("/claims/testnode1/day/rank[@order='1']") @doc.should_not have_xpath("/claims/testnode1/day/rank[@order='10']") @doc.should match_xpath("/claims/testnode1/day/rank[1]/@order", "1") @doc.should match_xpath("/claims/testnode1/day/rank[2]/@value", "0") @doc.should match_xpath("/claims/testnode1/day/rank[3]/@alias", "user54") @doc.should match_xpath("/claims/testnode1/day/sometext", "this is text") end endShow