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.
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.
Awesome! I was just about to my own custom matchers for XPath when I decided to google for an existing matcher. This is exactly what I was wanting.
Nice work!
It seems REXML's XPath does not work when you are specifying attribute conditions if there is a namespace declared in the xml.
I was able to fix this by adding an extra parameter to the matchers to accept a namespace hash to specify a namespace prefix.
For example, if your example xml had a namespace declared on claims:
`<claims xmlns='http://jimmyzimmerman.com/namespace'>`
then you need a namespace prefix on your elements like so:
`xml.should have_xpath("/z:claims/z:testnode1/z:day/z:rank[@order='1']", {'z' => 'http://jimmyzimmerman.com/namespace'})`
Feel free to email me if you would like a patch.
The github link in your update @top appears to be stale. Meanwhile, I'll try this version :)
I actually have a version I rewrote using hpricot, I'll post that too
There is a bug in MatchXpath... for invalid xpaths, REXML::XPath.each(doc, @xpath) results in an empty set, so the value ok never gets a chance to be set to false. (I'm assuming this is not the expected behavior... )
I'm on the road so I can't commit to github right now. Until I can, here are some failing specs...
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe Spec::Matchers::MatchXpath do
it 'should produce a list of nodes at the given xpath' do
matcher = Spec::Matchers::MatchXpath.new('/ride/info', 'x')
matcher.xml = '<ride><info>x</info><info>y</info></ride>'
matcher.node_values.should == ['x', 'y']
end
describe 'when matching a path that does not exist' do
before(:all) do
value = '2009-09-09T09:09:00+09:00'
@document = "<ride><end_time>#{value}</end_time></ride>"
@matcher = Spec::Matchers::MatchXpath.new('/invalid/end_time', value)
@matcher.matches?(@document)
end
it 'should not match' do
@matcher.matches?(@document).should be_false
end
it 'should indicate that there were no nodes in the failure message' do
@matcher.failure_message.should == 'There were no nodes at /invalid/end_time'
end
end
describe 'with a valid xpath and one matching node of many' do
before(:all) do
@document = "<ride><info>a</info><info>b</info></ride>"
@matcher = Spec::Matchers::MatchXpath.new('/ride/info', 'a')
@matcher.matches?(@document)
end
it 'should match' do
@matcher.matches?(@document).should be_true
end
end
end
and a new version of the class.....
class MatchXpath
def initialize(xpath, val)
@xpath = xpath
@val= val
end
def document
@xml.is_a?(REXML::Document) ? @xml : REXML::Document.new(@xml)
end
def nodes
REXML::XPath.match(document, @xpath)
end
def node_values
nodes.map do |node|
case node
when REXML::Attribute
node.to_s
when REXML::Element
node.text
else
node.to_s
end
end
end
def any_nodes_there?
(node_values.size > 0 )
end
def matching_nodes?
node_values.any?{|value| value==@val}
end
attr_accessor :xml
def matches?(response)
@xml = response
matching_nodes?
end
def failure_message
if matching_nodes?
"The xpath #{@xpath} did not have the value '#{@val}'
It was '#{@actual_val}'"
else
"There were no nodes at #{@xpath}"
end
end
def description
"match the xpath expression #{@xpath} with #{@val}"
end
end
enjoy.