I'm rspeccing some REST controllers which return XML, and wanting to use XPath to validate the responses.
I came across this
http://blog.wolfman.com/articles/2008/01/02/xpath-matchers-for-rspec
Thanks to him. It worked nicely (couldn't be bothered messing about with hpricot to get that to go), but I didn't like the API as much as I could have.
Example of that API:
response.body.should have_xpath('/root/node1')
response.body.should match_xpath('/root/node1', "expected_value" )
response.body.should have_nodes('/root/node1/child', 3 )
I didn't like the fact that there were 3 distinct matchers, and that match_xpath didn't work with regexes. I re-worked it, so the API is now
response.body.should have_xpath('/root/node1')
response.body.should have_xpath('/root/node1').with("expected_value") # can also pass a regex
response.body.should have(3).elements('/root/node1/child') # Note actually extends string class and uses normal rspec have matcher
Extending the String class to support elements(xpath) is a win also because it lets you do things like
response.body.elements('/child').each { |e| more complex assert for e here }
Without further ado, new code here:
# Code borrowed from
# http://blog.wolfman.com/articles/2008/01/02/xpath-matchers-for-rspec
# Modified to use one matcher and tweak syntax
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)
if @expected_value.nil?
not REXML::XPath.match(doc, @xpath).empty?
else # check each possible match for the right value
REXML::XPath.each(doc, @xpath) do |e|
@actual_value = e.is_a?(REXML::Element) ?
e.text :
e.to_s # handle REXML::Attribute and anything else
if @expected_value.kind_of?(Regexp) && @actual_value =~ @expected_value
return true
elsif @actual_value == @expected_value.to_s
return true
end
end
false # our loop didn't hit anything, mustn't be there
end
end
def with_value( val )
@expected_value = val
self
end
alias :with :with_value
def failure_message
if @expected_value.nil?
"Did not find expected xpath #{@xpath}"
else
"The xpath #{@xpath} did not have the value '#{@expected_value}'\nIt was '#{@actual_value}'"
end
end
def negative_failure_message
if @expected_value.nil?
"Found unexpected xpath #{@xpath}"
else
"Found unexpected xpath #{@xpath} matching value #{@expected_value}"
end
end
def description
"match the xpath expression #{@xpath}, optionally matching it's value"
end
end
def have_xpath(xpath)
HaveXpath.new(xpath)
end
# Utility function, so we can do this:
# response.body.should have(3).elements('/images/')
class ::String
def elements(xpath)
REXML::XPath.match( REXML::Document.new(self), xpath)
end
alias :element :elements
end
end
end
No comments:
Post a Comment