Testing Rails Partials

By Phlip Plumlee
October 9, 2008 | Comments: 11

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.

A Quick Guide to Rails

Rails Pocket ReferenceRails Pocket Reference offers a painless alternative to hunting for resources online, with brief yet thorough explanations of the most frequently used methods and structures supported by Rails 2.1, along with key concepts you need to work through the framework's most tangled corners.


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 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.


You might also be interested in:

11 Comments

Testing partials is like testing private methods. Why would you do this?

Because partials, like privates, might have bugs in them.

Tests should easily reach private methods. Privacy is a production code concern.

Philip,

First let me say that I agree with your comment to bryanl. I see nothing wrong with testing partials. The DRY principle, within reason, applies to test code as well as application code. Not only that, but Rails supports shared partials -- these are definitely not "private".

I have been getting more into RSpec lately and after reading your article I wanted to see how testing of partials can be done with RSpec. As you may know, RSpec supports separate view testing (spec'ing) from controller testing. With RSpec and it 's built-in mock support I found it way easier than the approach you had to take.

Here's a simple partial that I wanted to test:


<li><%= h keyword.phrase %></li>

And here's the view spec. For my money, it's much more intuitive and involves less hackery than the approach you had to take.


require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')

describe 'shared/_keyword.html.erb' do
    before(:each) do
        @keyword = mock_model(Keyword, :phrase => "tragopan")
        assigns[:keyword] = @keyword
    end

    it 'should display the keyword phrase in a list item' do
        @keyword.should_receive :phrase
        render :partial => 'shared/keyword', :locals => {:keyword => @keyword}
        response.should have_tag('li', "tragopan")
    end
end

I'll stand by my original statements. Testing partials still reeks like testing private methods. Shared or not, a partial can't stand on its own, so why don't you test in context?

Bill: My solution works for Rails as packaged, without any add-ons. And RSpec's 'def render' appears to contain more lines than my "hackery". Maybe I should try harder!

bryanl: CPUs all have little pads on them, so fabricators can test them on the die before packaging them. Car engines also have data ports, so mechanics can extract their private data. Using a back-door to test private modules is a Best Practice.

BTW a highly modified routes.rb might require this tweak:

if RAILS_ENV == 'test'
map.connect '/:controller/_renderizer', :action => '_renderizer'
end

Next, to pwn any irritating before_ or after_filters (the purview of a controller test, after all), neutralize them from the controller instance like this:

def render(args);
@controller.class.skip_before_filter @controller.class.before_filters
@controller.class.skip_after_filter @controller.class.after_filters
get :_renderizer, :args => args
reload = @controller.class.name.underscore
load RailsRoot + "app/controllers/#{reload}.rb"
end

Note that hackery squirms to restore the controller to its pristine condition...

If anyone would care to write render() without pulling in all of ZenTest or RSpec, I'm still really curious what the minimal version would be!

Keep up the excellent work! Your website helps to keep me from boredom as well.

Inspired by Phlip's idea, I came up with the following hackery, when I found that my local rails 2.2.2 changed keywords into string keys, which made render fail:

# test/test_helper.rb
class ApplicationController
  def _render 
    render(self._render_args)
  end
end

class ActionController::TestCase
def render(args)
class attr_accessor :_render_args
end
@controller._render_args = args
get "_render"
end
end

And HTML formatting tripped me up again ...
render should read

  def render(args)
    class && @controller
      attr_accessor :_render_args
    end
    @controller._render_args=args
    get "render"
  end

This thing needs a preview...
s.

thx i use this


cafedizi

News Topics

Recommended for You

Got a Question?