[A preview of my next blog entry]
Testing Rails Partials
One important metric, under Test Driven Development, is the distance
between a test case and its target code. Test cases use assertions to
observe events inside programs. If a test case requires more than a few
hops to reach its target event, the intermediate methods can add noise
to the test''s signal.
Some architectures make decoupling test cases very hard. This post
develops a fix for an icky Rails problem - testing one small partial
.rhtml file embedded in a huge web page.
Rails View Testing
Rails projects can test web pages by rendering them to HTML, then
diverting them into test cases. Rails functional tests can get
controller actions, then parse web pages, returned in @response.body,
to match important details.
(If your web pages are pure XHTML [a very good idea], you can test them
with assert_xpath. If they are not, call assert_tidy before
assert_xpath.)
Anything your production code pushes into a web page, with <%= %> eRB
tags, a test should pull out, using assert_match, assert_xpath, or
assert_select.
However, such tests can be noisy. A test that detects an important
number, such as 42 in an input field, should not trip over any
irrelevant 42s, such as a nearby <img width=''42''>.
When tests run
closer to their tested code, their signal gets stronger.
Rails can generate HTML by pushing .rhtml files (or .html.erb files)
together with layout files and partial files. A partial is Rails''s
unit
of HTML reuse. A Rails View can render a partial and insert it into its
hosting HTML like this:
<%= render :partial => "photos/show" %>
Test Driven Development works best when each test case targets one
aspect of a class''s interface. So this post will demonstrate a
simple
and direct way to test a partial without testing the Views, layouts,
and Controller actions surrounding it. On very complex projects, this
technique keeps your partials decoupled.
This is the Photo Gallery project from [1]Ajax on Rails, by Scott
Raymond. I upgraded it to use Rails 2.1, yet these techniques all work
freely with any Ruby on Rails version >1.4. Then I added a simple test
to its action that shows a gallery of thumbnails:
require File.dirname(__FILE__) + ''/../test_helper''
require ''albums_controller''
require ''assert_xpath''
require ''assert2''
class AlbumsController; def rescue_action(e) raise e end; end
class AlbumsControllerTest < ActionController::TestCase
include AssertXPath
fixtures :albums, :photos # add a couple real fixtures here first!
def test_show
album = albums(:first)
get :show, :id => album.id
assert_xpath :div, :photos, ''find <div
id="photos">'' do
assert_xpath :''ul/li'', album.photos.first.id,
''finds <ul><li
id="999">'' do
assert{ assert_xpath(:a)[:onclick] =~ /Photo.show/ }
end
end
end
end
The line with get :show, :id => album.id simulates a user hitting the
show action with the id of a photo album. The page comes back in the
secret variable @response.body with the rendered HTML.
An XPath DSL
The first assert_xpath converts that HTML into an XML document. The
notation :div, :photos is one of assert_xpath''s Domain Specific
Language shortcuts. It expands to the complete XPath ''//div[ @id
"photos" ]''. You could write all that too, if you wanted.
When assert_xpath''s first argument is a ''string'',
it evaluates as raw
XPath. When it''s a :symbol, assert_xpath tacks a // on the
beginning
(or the equivalent), meaning "seek any such node at or below the
current node".
Both forms of assert_xpath return only one node - the first one
encountered.
The last argument to assert_xpath is a diagnostic string. When
assert_xpath fails, it prints out this string, decorated with the
current HTML context.
When you call assert_xpath with a block, it narrows that context to
that block. Any assert_xpath calls inside that block can only match
HTML elements inside that container.
If the line assert_xpath :''ul/li''... failed, it would spew
out only the
contents of <div id=''photos''>. This is very
important in Web
development, because a complete HTML page could be several screens
long. Most of it would not relate to the tested feature.
In summary, that test case detects this HTML:
...<div id=''photos''>
<ul>
<li id=''520095529''>
<a onclick=''Photo.show...''>...</a>
</li>
</ul>
</div>...
I replaced the elements it did not detect with ... ellipses. Further
assertions could easily pin down their contents, if they were
important.
Cut to the Chase
The code which generated that HTML looks like this:
<div id="photos"><%= render :partial =>
"photos/index" %></div>
That looks mostly harmless, but imagine if all the other show.rhtml
business around it were heavy and expensive; fraught with side-effects.
Imagine if we needed to add an important feature into that partial,
requiring many test cases. Each test case would have to call extra code
to support those side-effects. Expensive test setup is a design smell -
it indicates code that''s too coupled. When tests run close to their
target code, they help it decouple.
Here''s how to test that partial directly:
class ApplicationController
def _renderizer; render params[:args]; end
end
class ActionController::TestCase # or Test::Unit::TestCase, for Rails <2.0
def render(args); get :_renderizer, :args => args; end
end
...
def test_photo_index
album = albums(:first)
render :partial => ''photos/index'',
:locals => { :@album => album }
assert_xpath :''ul/li'', album.photos.first.id,
''finds <ul><li
id="999">'' do
assert{ assert_xpath(:a)[:onclick] =~ /Photo.show/ }
end
end
That wasn''t too horrifying now was it?
In general, Rails''s internal architecture can be labyrinthine.
That''s
the price of incredible flexibility. Writing your own copy of render is
hard, because a test using ActionController::TestCase does not yet have
an ActiveView context. The test method get must concoct one using the
same procedures as a real web hit.
To bypass this problem, we first add a secret action to all controllers
- _renderizer. Because Ruby is extensible, the runtime application
never sees this method. It only appears under test. We implement render
by packing up its arguments, passing them thru get to _renderizer, and
letting it call render.
The benefit is our test case requires one less assertion. A more
complex application could have avoided much more cruft there. And if
our assert_xpath failed, now, its diagnostic would report only the
partial''s own contents.
And the mock render can generate any other View-level thing, isolating
it for test. For example, it could test that our application layout has
a link called "Gallery", like this:
def test_layout
render :layout => ''application'', :nothing => true
assert_xpath :''a[ . = "Gallery" ]''
end
This lightweight solution to a tricky Rails problem illustrates how
Ruby applications in general, and Rails in particular, reward thinking
outside the box.
References
1.
http://examples.oreilly.com/9780596527440/Ajax_on_Rails_Example_Applications.zip
--
Phlip
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"Ruby on Rails: Talk" group.
To post to this group, send email to
rubyonrails-talk-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org
To unsubscribe from this group, send email to
rubyonrails-talk+unsubscribe-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org
For more options, visit this group at
http://groups.google.com/group/rubyonrails-talk?hl=en
-~----------~----~----~----~------~----~------~--~---
Did you have a question? I might be mistaken... but I''m not sure the purpose of rails-talk is to submit your blog posts. On Sun, Oct 5, 2008 at 4:32 AM, Phlip <phlip2005-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org> wrote:> > [A preview of my next blog entry] > > Testing Rails Partials > > One important metric, under Test Driven Development, is the distance > between a test case and its target code. Test cases use assertions to > observe events inside programs. If a test case requires more than a few > hops to reach its target event, the intermediate methods can add noise > to the test''s signal. > > Some architectures make decoupling test cases very hard. This post > develops a fix for an icky Rails problem - testing one small partial > .rhtml file embedded in a huge web page. > > Rails View Testing > > Rails projects can test web pages by rendering them to HTML, then > diverting them into test cases. Rails functional tests can get > controller actions, then parse web pages, returned in @response.body, > to match important details. > > (If your web pages are pure XHTML [a very good idea], you can test them > with assert_xpath. If they are not, call assert_tidy before > assert_xpath.) > > Anything your production code pushes into a web page, with <%= %> eRB > tags, a test should pull out, using assert_match, assert_xpath, or > assert_select. > > However, such tests can be noisy. A test that detects an important > number, such as 42 in an input field, should not trip over any > irrelevant 42s, such as a nearby <img width=''42''>. When tests run > closer to their tested code, their signal gets stronger. > > Rails can generate HTML by pushing .rhtml files (or .html.erb files) > together with layout files and partial files. A partial is Rails''s unit > of HTML reuse. A Rails View can render a partial and insert it into its > hosting HTML like this: > <%= render :partial => "photos/show" %> > > Test Driven Development works best when each test case targets one > aspect of a class''s interface. So this post will demonstrate a simple > and direct way to test a partial without testing the Views, layouts, > and Controller actions surrounding it. On very complex projects, this > technique keeps your partials decoupled. > > This is the Photo Gallery project from [1]Ajax on Rails, by Scott > Raymond. I upgraded it to use Rails 2.1, yet these techniques all work > freely with any Ruby on Rails version >1.4. Then I added a simple test > to its action that shows a gallery of thumbnails: > > require File.dirname(__FILE__) + ''/../test_helper'' > require ''albums_controller'' > require ''assert_xpath'' > require ''assert2'' > > class AlbumsController; def rescue_action(e) raise e end; end > > class AlbumsControllerTest < ActionController::TestCase > include AssertXPath > fixtures :albums, :photos # add a couple real fixtures here first! > > def test_show > album = albums(:first) > get :show, :id => album.id > > assert_xpath :div, :photos, ''find <div id="photos">'' do > assert_xpath :''ul/li'', album.photos.first.id, > ''finds <ul><li id="999">'' do > assert{ assert_xpath(:a)[:onclick] =~ /Photo.show/ } > end > end > end > > end > > The line with get :show, :id => album.id simulates a user hitting the > show action with the id of a photo album. The page comes back in the > secret variable @response.body with the rendered HTML. > > An XPath DSL > > The first assert_xpath converts that HTML into an XML document. The > notation :div, :photos is one of assert_xpath''s Domain Specific > Language shortcuts. It expands to the complete XPath ''//div[ @id > "photos" ]''. You could write all that too, if you wanted. > > When assert_xpath''s first argument is a ''string'', it evaluates as raw > XPath. When it''s a :symbol, assert_xpath tacks a // on the beginning > (or the equivalent), meaning "seek any such node at or below the > current node". > > Both forms of assert_xpath return only one node - the first one > encountered. > > The last argument to assert_xpath is a diagnostic string. When > assert_xpath fails, it prints out this string, decorated with the > current HTML context. > > When you call assert_xpath with a block, it narrows that context to > that block. Any assert_xpath calls inside that block can only match > HTML elements inside that container. > > If the line assert_xpath :''ul/li''... failed, it would spew out only the > contents of <div id=''photos''>. This is very important in Web > development, because a complete HTML page could be several screens > long. Most of it would not relate to the tested feature. > > In summary, that test case detects this HTML: > ...<div id=''photos''> > <ul> > <li id=''520095529''> > <a onclick=''Photo.show...''>...</a> > </li> > </ul> > </div>... > > I replaced the elements it did not detect with ... ellipses. Further > assertions could easily pin down their contents, if they were > important. > > Cut to the Chase > > The code which generated that HTML looks like this: > > <div id="photos"><%= render :partial => "photos/index" %></div> > > That looks mostly harmless, but imagine if all the other show.rhtml > business around it were heavy and expensive; fraught with side-effects. > Imagine if we needed to add an important feature into that partial, > requiring many test cases. Each test case would have to call extra code > to support those side-effects. Expensive test setup is a design smell - > it indicates code that''s too coupled. When tests run close to their > target code, they help it decouple. > > Here''s how to test that partial directly: > > class ApplicationController > def _renderizer; render params[:args]; end > end > > class ActionController::TestCase # or Test::Unit::TestCase, for Rails <2.0 > def render(args); get :_renderizer, :args => args; end > end > > ... > def test_photo_index > album = albums(:first) > > render :partial => ''photos/index'', > :locals => { :@album => album } > > assert_xpath :''ul/li'', album.photos.first.id, > ''finds <ul><li id="999">'' do > assert{ assert_xpath(:a)[:onclick] =~ /Photo.show/ } > end > end > > That wasn''t too horrifying now was it? > > In general, Rails''s internal architecture can be labyrinthine. That''s > the price of incredible flexibility. Writing your own copy of render is > hard, because a test using ActionController::TestCase does not yet have > an ActiveView context. The test method get must concoct one using the > same procedures as a real web hit. > > To bypass this problem, we first add a secret action to all controllers > - _renderizer. Because Ruby is extensible, the runtime application > never sees this method. It only appears under test. We implement render > by packing up its arguments, passing them thru get to _renderizer, and > letting it call render. > > The benefit is our test case requires one less assertion. A more > complex application could have avoided much more cruft there. And if > our assert_xpath failed, now, its diagnostic would report only the > partial''s own contents. > > And the mock render can generate any other View-level thing, isolating > it for test. For example, it could test that our application layout has > a link called "Gallery", like this: > > def test_layout > render :layout => ''application'', :nothing => true > assert_xpath :''a[ . = "Gallery" ]'' > end > > This lightweight solution to a tricky Rails problem illustrates how > Ruby applications in general, and Rails in particular, reward thinking > outside the box. > > References > > 1. > http://examples.oreilly.com/9780596527440/Ajax_on_Rails_Example_Applications.zip > > -- > Phlip > > > > >-- Robby Russell Chief Evangelist, Partner PLANET ARGON, LLC design // development // hosting http://www.planetargon.com/ http://www.robbyonrails.com/ aim: planetargon +1 503 445 2457 +1 877 55 ARGON [toll free] +1 815 642 4068 [fax] --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group. To post to this group, send email to rubyonrails-talk-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org To unsubscribe from this group, send email to rubyonrails-talk+unsubscribe-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org For more options, visit this group at http://groups.google.com/group/rubyonrails-talk?hl=en -~----------~----~----~----~------~----~------~--~---
> [A preview of my next blog entry]Aaaaand - if I can trust the new blogging system there - my next blog entry is up: http://broadcast.oreilly.com/2008/10/testing-rails-partials.html -- Phlip --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group. To post to this group, send email to rubyonrails-talk-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org To unsubscribe from this group, send email to rubyonrails-talk+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-talk?hl=en -~----------~----~----~----~------~----~------~--~---