Re: [TFUI] asserting HTML by example
Jason Citron <[email protected]> Wed, 11 Mar 2009 22:33:22 -0700
| Newsgroups | gmane.comp.programming.test-first-user-interfaces |
|---|---|
| Message-ID | <[email protected]> |
--EYE-5-e-vYm-5XuxFHl25sXLf9znU57-MrxWjbW Content-Type: text/plain; charset=US-ASCII Content-Transfer-Encoding: 7bit For structural HTML testing you should check out assert_select. It comes with Rails 2.0. Useful for testing XML too. http://api.rubyonrails.org/classes/ActionController/Assertions/SelectorAssertions.html On Wed, Mar 11, 2009 at 10:27 PM, Phlip <[email protected]> wrote: > extremists: > > HTML is easy to test and hard to test well. A unit test must intercept and > parse > it, hopefully _before_ it goes thru a web server. > > I just wrote an assertion which turns that problem on its head. Instead of > asserting how to parse that HTML, we will assert how to build an example of > HTML > that the production HTML must match. > > This post is the high-level, language-agnostic, overview of these > announcement > posts: > > http://www.ruby-forum.com/topic/181145#792950 > > Here's the example: > > user = users(:Moses) > get :edit_user, :id => user.id # fetches a web page > > assert_xhtml do > > form :action => '/users' do > fieldset do > legend 'Personal Information' > label 'First name' > input :type => 'text', > :name => 'user[first_name]' > :value => user.first_name > end > end > > end > > That's all. Inside the assertion's block, you declare the HTML you want, > using a > Builder notation. (Nokogiri, in Ruby's case.) > > The assertion skips over any intervening elements, such as <ul>, <table>, > etc, > that the block did not build. In the above example, <fieldset> only needs > to > appear somewhere inside the <form> - not necessarily its first element. But > > those <label> and <input> fields must appear somewhere inside that > <fieldset>, > and in the given order! > > To create this assertion in your language, start with your nearest HTML > builder. > Allow the user-programmer to build an example, then traverse its DOM, and > convert each item into an XPath seeking that item in the production HTML. > Declare a match if all the specified attributes match, and if most of the > text > matches - disregarding blanks. > > Then, for each match, recursively match each built child element, applying > its > XPath relative to the current node in the production HTML's DOM. > > When you find a fault, build an error message by rendering the current > nodes to > HTML, and return this out of your loop statements. This Gist explicates the > > algorithm: > > http://gist.github.com/76136 > > The algorithm essentially matches two trees, depth-first and in-order. The > example HTML must be a subset of the production HTML. > > If you survive all that recursing and matching then your assertion passes. > You > verified a huge number of details, using extremely lean code, and you > ignored > many more details, allowing them to change freely as you upgrade your > website. > > -- > Phlip > http://www.zeroplayer.com/ > > [Non-text portions of this message have been removed] --EYE-5-e-vYm-5XuxFHl25sXLf9znU57-MrxWjbW Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> </head> <body style="background-color: #ffffff;"> <!--~-|**|PrettyHtmlStartT|**|-~--> <div id="ygrp-mlmsg" style="width:655px; position:relative;"> <div id="ygrp-msg" style="width: 470px; margin:0; padding:0 25px 0 0; float:left; z-index:1;"> <!--~-|**|PrettyHtmlEndT|**|-~--> <div id="ygrp-text"> <p>For structural HTML testing you should check out assert_select. It comes<br> with Rails 2.0.<br> <br> Useful for testing XML too.<br> <br> <a href="http://api.rubyonrails.org/classes/ActionController/Assertions/SelectorAssertions.html">http://api.rubyonra<wbr>ils.org/classes/<wbr>ActionController<wbr>/Assertions/<wbr>SelectorAssertio<wbr>ns.html</a><br> <br> On Wed, Mar 11, 2009 at 10:27 PM, Phlip <<a href="mailto:phlip2005%40gmail.com">phlip2005@gmail.<wbr>com</a>> wrote:<br> <br> > extremists:<br> ><br> > HTML is easy to test and hard to test well. A unit test must intercept and<br> > parse<br> > it, hopefully _before_ it goes thru a web server.<br> ><br> > I just wrote an assertion which turns that problem on its head. Instead of<br> > asserting how to parse that HTML, we will assert how to build an example of<br> > HTML<br> > that the production HTML must match.<br> ><br> > This post is the high-level, language-agnostic, overview of these<br> > announcement<br> > posts:<br> ><br> > <a href="http://www.ruby-forum.com/topic/181145#792950">http://www.ruby-<wbr>forum.com/<wbr>topic/181145#<wbr>792950</a><br> ><br> > Here's the example:<br> ><br> > user = users(:Moses)<br> > get :edit_user, :id => user.id # fetches a web page<br> ><br> > assert_xhtml do<br> ><br> > form :action => '/users' do<br> > fieldset do<br> > legend 'Personal Information'<br> > label 'First name'<br> > input :type => 'text',<br> > :name => 'user[first_<wbr>name]'<br> > :value => user.first_name<br> > end<br> > end<br> ><br> > end<br> ><br> > That's all. Inside the assertion's block, you declare the HTML you want,<br> > using a<br> > Builder notation. (Nokogiri, in Ruby's case.)<br> ><br> > The assertion skips over any intervening elements, such as <ul>, <table>,<br> > etc,<br> > that the block did not build. In the above example, <fieldset> only needs<br> > to<br> > appear somewhere inside the <form> - not necessarily its first element. But<br> ><br> > those <label> and <input> fields must appear somewhere inside that<br> > <fieldset>,<br> > and in the given order!<br> ><br> > To create this assertion in your language, start with your nearest HTML<br> > builder.<br> > Allow the user-programmer to build an example, then traverse its DOM, and<br> > convert each item into an XPath seeking that item in the production HTML.<br> > Declare a match if all the specified attributes match, and if most of the<br> > text<br> > matches - disregarding blanks.<br> ><br> > Then, for each match, recursively match each built child element, applying<br> > its<br> > XPath relative to the current node in the production HTML's DOM.<br> ><br> > When you find a fault, build an error message by rendering the current<br> > nodes to<br> > HTML, and return this out of your loop statements. This Gist explicates the<br> ><br> > algorithm:<br> ><br> > <a href="http://gist.github.com/76136">http://gist.<wbr>github.com/<wbr>76136</a><br> ><br> > The algorithm essentially matches two trees, depth-first and in-order. The<br> > example HTML must be a subset of the production HTML.<br> ><br> > If you survive all that recursing and matching then your assertion passes.<br> > You<br> > verified a huge number of details, using extremely lean code, and you<br> > ignored<br> > many more details, allowing them to change freely as you upgrade your<br> > website.<br> ><br> > --<br> > Phlip<br> > <a href="http://www.zeroplayer.com/">http://www.zeroplay<wbr>er.com/</a><br> > <br> ><br> <br> [Non-text portions of this message have been removed]<br> <br> </p> </div> <!--~-|**|PrettyHtmlStart|**|-~--> <div width="1" style="color: white; clear: both;">__._,_.___</div> <!-- Start the section with Message In topic --> <div id="ygrp-actbar" style="clear: both; margini-bottom: 25px; white-space:nowrap; color: #666; text-align: right;"> <span class="left" style="float: left; white-space:nowrap;"> <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/message/1037;_ylc=X3oDMTM0ZGI5NWFsBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BG1zZ0lkAzEwMzgEc2VjA2Z0cgRzbGsDdnRwYwRzdGltZQMxMjM2ODM2MDI1BHRwY0lkAzEwMzc-"> Messages in this topic </a> (<span class="bld" style="font-weight:bold;">2</span>) </span> <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/post;_ylc=X3oDMTJwNW85a25nBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BG1zZ0lkAzEwMzgEc2VjA2Z0cgRzbGsDcnBseQRzdGltZQMxMjM2ODM2MDI1?act=reply&messageNum=1038"> <span class="bld" style="font-weight:bold;"> Reply </span> (via web post) </a> | <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/post;_ylc=X3oDMTJlZnRwYTFyBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA250cGMEc3RpbWUDMTIzNjgzNjAyNQ--" class="bld" style="font-weight:bold;"> Start a new topic </a> </div> <!------- Start Nav Bar ------> <!-- |**|begin egp html banner|**| --> <div id="ygrp-vitnav" style="padding-top: 10px; font-family: Verdana; font-size: 77%; margin: 0;"> <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/messages;_ylc=X3oDMTJlN2VxNmQ3BF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA21zZ3MEc3RpbWUDMTIzNjgzNjAyNQ--">Messages</a> | <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/files;_ylc=X3oDMTJmNWhpZjk3BF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA2ZpbGVzBHN0aW1lAzEyMzY4MzYwMjU-">Files</a> | <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/photos;_ylc=X3oDMTJlamRjdDFqBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA3Bob3QEc3RpbWUDMTIzNjgzNjAyNQ--">Photos</a> | <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/links;_ylc=X3oDMTJmaDBhbDNkBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA2xpbmtzBHN0aW1lAzEyMzY4MzYwMjU-">Links</a> | <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/database;_ylc=X3oDMTJjMmFlZThxBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA2RiBHN0aW1lAzEyMzY4MzYwMjU-">Database</a> | <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/polls;_ylc=X3oDMTJmNjQ5bHQzBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA3BvbGxzBHN0aW1lAzEyMzY4MzYwMjU-">Polls</a> | <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/members;_ylc=X3oDMTJlY2Q4OWNyBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA21icnMEc3RpbWUDMTIzNjgzNjAyNQ--">Members</a> | <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/calendar;_ylc=X3oDMTJkbmFmaGU3BF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA2NhbARzdGltZQMxMjM2ODM2MDI1">Calendar</a> </div> <!-- |**|end egp html banner|**| --> <div id="ygrp-grfd" style="font-family: Verdana; font-size: 77%; padding: 15px 0;"> </div> <!-- yahoo logo --> <!-- |**|begin egp html banner|**| --> <div id="ygrp-ft" style="font-family: verdana; font-size: 77%; border-top: 1px solid #666; padding: 5px 0;"> <a href="http://groups.yahoo.com/;_ylc=X3oDMTJkdWJiZ2YwBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA2dmcARzdGltZQMxMjM2ODM2MDI1"> <img src="http://us.i1.yimg.com/us.yimg.com/i/yg/img/logo/ma_grp_160.gif" height="15" width="106" border="0" alt="Yahoo! Groups"></a> <br> <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/join;_ylc=X3oDMTJmMWE3Njg4BF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA3N0bmdzBHN0aW1lAzEyMzY4MzYwMjU-">Change settings via the Web</a> (Yahoo! ID required) <br> Change settings via email: <a href="mailto:TestFirstUserInterfaces-digest-hHKSG33TihhbjbujkaE4pw@public.gmane.org?subject=Email Delivery: Digest">Switch delivery to Daily Digest</a> | <a href = "mailto:TestFirstUserInterfaces-traditional-hHKSG33TihhbjbujkaE4pw@public.gmane.org?subject=Change Delivery Format: Traditional">Switch format to Traditional</a> <br> <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces;_ylc=X3oDMTJkMjl0c29wBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA2hwZgRzdGltZQMxMjM2ODM2MDI1"> Visit Your Group </a> | <a href="http://docs.yahoo.com/info/terms/"> Yahoo! Groups Terms of Use </a> | <a href="mailto:TestFirstUserInterfaces-unsubscribe-hHKSG33TihhbjbujkaE4pw@public.gmane.org?subject="> Unsubscribe </a> </div> <!-- |**|end egp html banner|**| --> </div> <!-- ygrp-msg --> <!-- Sponsor --> <!-- |**|begin egp html banner|**| --> <div id="ygrp-sponsor" style="width:160px; float:right; clear:none; margin:0 0 25px 0; background: white;"> <!-- Network content --> <!-- Start Recommendations --> <div id="ygrp-reco"> </div> <!-- End Recommendations --> <!-- Start vitality --> <div id="ygrp-vital" style="background-color: #e0ecee; margin-bottom: 20px; padding: 2px 0 8px 8px;"> <div id="vithd" style="font-size: 77%; font-family: Verdana; font-weight: bold; color: #333; text-transform: uppercase;">Recent Activity</div> <ul style="list-style-type:none; padding: 0; margin: 2px 0;"> </ul> <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces;_ylc=X3oDMTJlZXFxdHNxBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwN2dGwEc2xrA3ZnaHAEc3RpbWUDMTIzNjgzNjAyNQ--"> Visit Your Group </a> </div> <!-- Network content --> <div style="background-color:#EEEEEE; margin-bottom:20px; padding:10px 8px;"> <div id="nc"> <div class="ad" style="color:#628C2A; font-family:Arial; font-weight:bold;"> <div id="hd1">Give Back</div> <p><a href="http://us.lrd.yahoo.com/_ylc=X3oDMTJtZGtuaDBzBF9TAzk3MzU5NzE0BF9wAzEEZ3JwSWQDODc0NDcwNARncnBzcElkAzE3MDUwMDcyMDcEc2VjA25jbW9kBHNsawNicmFuZARzdGltZQMxMjM2ODM2MDI1;_ylg=1/SIG=11314uv3k/**http%3A//brand.yahoo.com/forgood" style="text-decoration:none; color:#1E66AE; font-weight: normal;">Yahoo! for Good</a></p> <p style="color: #000000; font-weight: normal;">Get inspired</p> <p style="color: #000000; font-weight: normal;">by a good cause.</p> </div> <div class="ad" style="color:#628C2A; font-family:Arial; font-weight:bold;"> <div id="hd1">Y! Toolbar</div> <p><a href="http://us.lrd.yahoo.com/_ylc=X3oDMTJvZmJuZ2FwBF9TAzk3MzU5NzE0BF9wAzIEZ3JwSWQDODc0NDcwNARncnBzcElkAzE3MDUwMDcyMDcEc2VjA25jbW9kBHNsawN0b29sYmFyBHN0aW1lAzEyMzY4MzYwMjU-;_ylg=1/SIG=11c6dvmk9/**http%3A//toolbar.yahoo.com/%3F.cpdl=ygrps" style="text-decoration:none; color:#1E66AE; font-weight: normal;">Get it Free!</a></p> <p style="color: #000000; font-weight: normal;">easy 1-click access</p> <p style="color: #000000; font-weight: normal;">to your groups.</p> </div> <div class="ad" style="color:#628C2A; font-family:Arial; font-weight:bold;"> <div id="hd1">Yahoo! Groups</div> <p><a href="http://groups.yahoo.com/start;_ylc=X3oDMTJvYmlhbTYzBF9TAzk3MzU5NzE0BF9wAzMEZ3JwSWQDODc0NDcwNARncnBzcElkAzE3MDUwMDcyMDcEc2VjA25jbW9kBHNsawNncm91cHMyBHN0aW1lAzEyMzY4MzYwMjU-" style="text-decoration:none; color:#1E66AE; font-weight: normal;">Start a group</a></p> <p style="color: #000000; font-weight: normal;">in 3 easy steps.</p> <p style="color: #000000; font-weight: normal;">Connect with others.</p> </div> </div> </div> </div> <!-- |**|end egp html banner|**| --> <div style="clear:both; color: #FFF; font-size:1px;">.</div> </div> <img src="http://geo.yahoo.com/serv?s=97359714/grpId=8744704/grpspId=1705007207/msgId=1038/stime=1236836025/nc1=1/nc2=2/nc3=3" width="1" height="1"> <br> <div style="color: white; clear: both;">__,_._,___</div> <!--~-|**|PrettyHtmlEnd|**|-~--> </body> <!--~-|**|PrettyHtmlStart|**|-~--> <head> <style type="text/css"> <!-- #ygrp-mkp{ border: 1px solid #d8d8d8; font-family: Arial; margin: 14px 0px; padding: 0px 14px; } #ygrp-mkp hr{ border: 1px solid #d8d8d8; } #ygrp-mkp #hd{ color: #628c2a; font-size: 85%; font-weight: bold; line-height: 122%; margin: 10px 0px; } #ygrp-mkp #ads{ margin-bottom: 10px; } #ygrp-mkp .ad{ padding: 0 0; } #ygrp-mkp .ad a{ color: #0000ff; text-decoration: none; } --> </style> </head> <head> <style type="text/css"> <!-- #ygrp-sponsor #ygrp-lc{ font-family: Arial; } #ygrp-sponsor #ygrp-lc #hd{ margin: 10px 0px; font-weight: bold; font-size: 78%; line-height: 122%; } #ygrp-sponsor #ygrp-lc .ad{ margin-bottom: 10px; padding: 0 0; } --> </style> </head> <head> <style type="text/css"> <!-- #ygrp-mlmsg {font-size:13px; font-family: arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small;} #ygrp-mlmsg table {font-size:inherit;font:100%;} #ygrp-mlmsg select, input, textarea {font:99% arial,helvetica,clean,sans-serif;} #ygrp-mlmsg pre, code {font:115% monospace;*font-size:100%;} #ygrp-mlmsg * {line-height:1.22em;} #ygrp-text{ font-family: Georgia; } #ygrp-text p{ margin: 0 0 1em 0; } dd.last p a { font-family: Verdana; font-weight: bold; } #ygrp-vitnav{ padding-top: 10px; font-family: Verdana; font-size: 77%; margin: 0; } #ygrp-vitnav a{ padding: 0 1px; } #ygrp-mlmsg #logo{ padding-bottom: 10px; } #ygrp-reco { margin-bottom: 20px; padding: 0px; } #ygrp-reco #reco-head { font-weight: bold; color: #ff7900; } #reco-category{ font-size: 77%; } #reco-desc{ font-size: 77%; } #ygrp-vital a{ text-decoration: none; } #ygrp-vital a:hover{ text-decoration: underline; } #ygrp-sponsor #ov ul{ padding: 0 0 0 8px; margin: 0; } #ygrp-sponsor #ov li{ list-style-type: square; padding: 6px 0; font-size: 77%; } #ygrp-sponsor #ov li a{ text-decoration: none; font-size: 130%; } #ygrp-sponsor #nc{ background-color: #eee; margin-bottom: 20px; padding: 0 8px; } #ygrp-sponsor .ad{ padding: 8px 0; } #ygrp-sponsor .ad #hd1{ font-family: Arial; font-weight: bold; color: #628c2a; font-size: 100%; line-height: 122%; } #ygrp-sponsor .ad a{ text-decoration: none; } #ygrp-sponsor .ad a:hover{ text-decoration: underline; } #ygrp-sponsor .ad p{ margin: 0; } o{font-size: 0; } .MsoNormal{ margin: 0 0 0 0; } #ygrp-text tt{ font-size: 120%; } blockquote{margin: 0 0 0 4px;} .replbq{margin:4} dd.last p span { margin-right: 10px; font-family: Verdana; font-weight: bold; } dd.last p span.yshortcuts { margin-right: 0; } div.photo-title a, div.photo-title a:active, div.photo-title a:hover, div.photo-title a:visited { text-decoration: none; } div.file-title a, div.file-title a:active, div.file-title a:hover, div.file-title a:visited { text-decoration: none; } #ygrp-msg p { clear: both; padding: 15px 0 3px 0; overflow: hidden; } #ygrp-msg p span { color: #1E66AE; font-weight: bold; } div#ygrp-mlmsg #ygrp-msg p a span.yshortcuts { font-family: Verdana; font-size: 10px; font-weight: normal; } #ygrp-msg p a { font-family: Verdana; font-size: 10px; } #ygrp-mlmsg a { color: #1E66AE; } div.attach-table div div a { text-decoration: none; } div.attach-table { width: 400px; } --> </style> </head> <!--~-|**|PrettyHtmlEnd|**|-~--> </html><!--End group email --> --EYE-5-e-vYm-5XuxFHl25sXLf9znU57-MrxWjbW--