XPath matchers for rspec
Posted by Jim Morris Wed, 02 Jan 2008 21:32:50 GMT
UPDATE I found this which is a nicer way of doing it using hpricot, which is faster. I have created a slightly different version that does have_xpath, for XML.
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}'\nIt 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}\nFound #{@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
end