xpath{} tests XML and XHTML
xpath{} works closely with
assert{ 2.0 }
to provide elaborate, detailed, formatted reports when your XHTML code has
gone astray.
Installation
xpath{} bundles with assert{ 2.0 }
and assert{ 2.1 }.
Install
one of them first, then
require 'assert2/xpath'
That also imports assert{ 2.0 }.
Strategy
First, generate your XHTML, then pass it into
assert_xhtml() . Use
_assert_xml() if all you have is a fragment of XHTML, or XML from some other schema.
Use a call like title = xpath('/html/head/title')
alone, without assert{}, to
extract XML nodes non-judgementally. It returns a nil
when it fails. The returned node will provide
text and
attribute helper methods.
To learn XPath, read "XPath Checker and assert_xpath", and attach an XPath tool like XPath Checker or XPather to your Firefox web browser.
Then wrap your xpath() calls in assert{} to
verify their details. For example, if your production code generates a
<form> with a useful edit field in it, you can
capture the
field like this:
assert{ xpath('//input[ @type = "text" and @name = "user" and
@value = "Roosevelt Franklin" ]') }
You can program xpath() using XPath notation, or convenient
Ruby option-hash
notation. The equivalent query is:
assert{ xpath(:input, :type => :text, :name => :user",
:value => 'Roosevelt Franklin') }
When these assertions fail, xpath() works closely with
assert{} to present a detailed, formatted, readable report
of the XML node that failed, and its relevant context within a document.
xpath{} can evaluate a block; if this contains more
xpath() calls, they
nest inside the outer xpath{}'s context. This lets you skip
over irrelevant details in a document, and only test the relevant
regions.
If a nested xpath{} fails, the diagnostic only
reflects the
inner context. This is you don't have to read an entire document to find the
part that failed.
assert_xhtml( xhtml )
Use this method to push well-formed XHTML into the assertion system. Subsequent
xpath() calls will interrogate this XHTML, using XPath notation:
def test_assert_xhtml assert_xhtml '<html><body><div id="forty_two">42</div></body></html>' assert{ xpath('//div[ @id = "forty_two" ]').text == '42' } end
_assert_xml()
Some versions of assert_xhtml() fuss when
passed an XHTML fragment, or XML under some other schema. Use
_assert_xml() to bypass those conveniences:
def test__assert_xml _assert_xml '<Mean><Woman>Blues</Woman></Mean>' assert{ xpath('/Mean/Woman').text == 'Blues' } end
xpath( 'path' )
The function's first argument can be raw XPath in a string, or a symbol. The first
symbol gets decorated
with the 'descendant-or-self::' XPath axis, and a second symbol,
or an option hash,
get converted into a predicate, like [ @id = "forty_two" ].
All the following queries reach out to the same node. Prefer the last notation, to cut thru a large XHTML web page down to the element containing the contents that you need to test:
def test_assert_xpath assert_xhtml '<html><body><div id="forty_two">42</div></body></html>' assert{ xpath('descendant-or-self::div[ @id = "forty_two" ]').text == '42' } assert{ xpath('//div[ @id = "forty_two" ]').text == '42' } assert{ xpath(:'div[ @id = "forty_two" ]').text == '42' } assert{ xpath(:div, :id => :forty_two).text == '42' } assert{ xpath(:div, :forty_two).text == '42' } end
xpath( DSL )
You can write simple XPath queries using Ruby's familiar hash notation. Query
a node's string contents with ?.:
def test_xpath_dsl assert_xhtml 'hit <a href="http://antwrp.gsfc.nasa.gov/apod/" >apod</a> daily!' assert do xpath :a, :href => 'http://antwrp.gsfc.nasa.gov/apod/', ?. => 'apod' # the?.resolves to XPath:a[ . = "apod" ]end end
xpath( ID shortcut )
When xpath()'s second argument is a :symbol,
it expands to [ @id = "symbol" ]. Subsequent arguments
use Hash notation:
def test_xpath_ID_shortcut assert_xhtml '<html><body> <div id="forty_two" class="answer_to_the_great_question"> 42 </div> </body></html>' div_1 = xpath(:div, :forty_two, :class => :answer_to_the_great_question) div_2 = xpath(:div, :id => :forty_two, :class => :answer_to_the_great_question) assert{ div_1.text =~ /42/ and div_1 == div_2 } end
xpath().text
xpath() returns the first matching
REXML::Node object
(or nil if it found none). The object has a useful method, .text,
which returns the nearest text contents:
def test_xpath_text _assert_xml '<Mean><Woman>Blues</Woman></Mean>' assert do xpath('/Mean/Woman').text == 'Blues' end end
xpath() Attribute Accessors
Raw REXML::Nodes require a cluttered syntax
to access node attributes. Because xpath()
supports expedient queries, it adds a Hash-like accessor
to returned nodes:
def test_indent_xml
_assert_xml '<a href="http://www.youtube.com/watch?v=lWqr3mFAJ0Y"
>YouTube - UB40 - Sardonicus</a>'
xpath '/a' do |a|
a.attributes['href'] =~ /youtube/ and # raw REXML::Node
a['href'] =~ /youtube/ and # permitted by xpath()
a[:href ] =~ /youtube/ # convenient!
end
end
Nested xpath{}
xpath{} takes a block, and passes this to
assert{}. Any further xpath() calls, inside this
block, evaluate within the XML context set by the outer block.
This is useful because if you have a huge web page (such as the one you are reading)
you need assertions that operate on one small region alone - the place where a feature
must appear. You can typically give all your <div> regions
useful ids, then use xpath(:div, :my_id) to restrict further
xpath{} calls:
def test_nested_xpaths assert_xhtml (DocPath + 'assert2_xpath.html').read assert 'this tests the panel you are now reading' do xpath :a, :name => :Nested_xpath do # finds the panel's anchor xpath '../following-sibling::div[1]' do # finds thatatag's adjacent sibling xpath :'pre/span', ?. => 'test_nested_xpaths' do |span| span.text =~ /nested/ # the block passes the target node thru the|goalposts|end end end end end
xpath{ block } Calls assert{ block }
When xpath{} has a block, it passes its
detected |node| into
the block. If the
block returns nil or false, it will flunk()
the block, using assert{}'s inner mechanics:
def test_xpath_passes_its_block_to_assert_2 _assert_xml '<tag>contents</tag>' assert_flunk /text.* --> "contents"/ do xpath '/tag' do |tag| tag.text == 'wrong contents!' end end end
Nested xpath{} Faults
When an inner xpath{} fails, the diagnostic's "xml context:"
field contains only the inner XML. This prevents excessive spew when testing entire
web pages:
def test_nested_xpath_faults assert_xhtml (DocPath + 'assert2_xpath.html').read diagnostic = assert_flunk /BAD.*CONTENTS/ do xpath :a, :name => :Nested_xpath_Faults do # the../finds this entire panel, thediv[1]finds its content area, # and theprefinds the code sample you are reading xpath '../following-sibling::div[1]/pre' do # this will fail because that text ain't found in aspan# (theconcat()makes it into twospans!) xpath :'span[ . = concat("BAD", " CONTENTS") ]' end end end # puts diagnostic # the diagnostic won't cantain the string "excessive spew", from # the top of the panel, because the secondxpath{}call excluded it deny{ diagnostic =~ /excessive spew/ } # FIXME uh, this is the wrong context!! end
xpath( ?. ) Matches Recursive Text
When an XML node contains child nodes with text,
the XPath predicate [ . = "..." ] matches
all their text, concatenated together. xpath()'s
DSL converts ?. into that notation:
def test_nested_xpath_text _assert_xml '<boats><a>frig</a><b>ates</b></boats>' assert{ xpath :boats, ?. => 'frigates' } end
xpath( ?. ) Notation Is not the Same as xpath().text
xpath() returns the first node, in document order, which
matches its XPath arguments. So ?. will
force xpath() to keep searching for a hit.
xpath().text will find the first matching node, then offer
its .text for comparison. These assertions explicate
the difference:
def test_xpath_text_is_not_the_same_as_question_dot_notation _assert_xml '<Mean> <Woman>Blues</Woman> <Woman>Dub</Woman> </Mean>' assert do xpath(:Woman).text == 'Blues' and xpath(:Woman, ?. => :Dub).text == 'Dub' # use a symbol ^ to match a string here, as a convenience end end
Use indent_xml to Help Build your XPath
To see what region xpath{} has selected in
a big document, temporarily add puts
indent_xml and
to xpath's block, then run your tests:
def test_indent_xml
assert_xhtml (DocPath + 'assert2_xpath.html').read
xpath :span, ?. => :test_indent_xml do
xpath '..' do
# puts indent_xml and # Decomment this to see where you are in the document now
indent_xml.match("<pre>\n") and
! indent_xml.match('<html')
end
end
end