Rails, Ajax, RJS, and Testing
As promised, today’s entry is Agile and Ajax. Also, it’s a dessert topping.
Ajax in Rails
The initial support for Ajax calls in Rails was centered on two framework methods called link_to_remote and remote_form_tag. The basic functionality of these methods is to allow a remote call to be triggered by a link or form submit and have the result of that call be used to update a DOM element somewhere on the page.
Moving outside that simple behavior, however, quickly got messy. Both methods specify a series of callbacks where arbitrary JavaScript can get implemented at various points in the call life-cycle. However, writing dynamic JavaScript in an ERB template is awkward, and the resulting method calls could get ugly.
Enter Ruby JavaScript (RJS), sometimes described as “training wheels for JavaScript”. RJS allows a very simple subset of JavaScript to be written in Ruby and translated to JavaScript as the result of a Rails Ajax Request.
Here’s what a sample RJS template looks like:
page.visual_effect(:fade, 'recipes_to_show', :duration => 0.5)
page.visual_effect(:fade, 'category_being_shown', :duration => 0.5)
page.delay 0.5 do
page.replace_html("category_being_shown",
"Recipies For: #{@category.capitalize}")
page.replace_html("recipes_to_show", :partial => "recipes")
page.visual_effect(:appear, 'recipes_to_show', :duration => 0.5)
page.visual_effect(:appear, 'category_being_shown', :duration => 0.5)
end
The page variable is automatically provided by Rails and represents the page that is going to receive the JavaScript. There are about a dozen or so instance methods that page can receive, mostly having to do with the basic Ajax-y features of replacing HTML in a DOM element and calling Scriptaculous visual effects. This particular snippet fades out two elements, changes their text, and fades them back in for a crossfade effect.
(There’s a certain similarity of concept with Google Web Toolkit, but RJS is going after a much smaller and more focused piece of functionality, optimized towards what you would do in a single Ajax call in an otherwise standard interface. GWT, on the other hand, is trying to be the entire application on both the client and server side.)
How useful this is depends on your relative level of comfort with Ruby and JavaScript. I’m personally much more comfortable in Ruby, so I think this is just great. It’s especially nice since I can use Ruby blocks to abstract this crossfade function into something I can use generically.
def crossfade(page, *dom_ids)
dom_ids.each do |dom_id|
page.visual_effect(:fade, dom_id, :duration => 0.5)
end
page.delay 0.5 do
yield
dom_ids.each do |dom_id|
page.visual_effect(:appear, dom_id, :duration => 0.5)
end
end
end
The generic function takes a list of DOM ids and a block — the assumption is that all the content changing will be managed in the block. So the original snippet would now be changed to:
crossfade(page, 'recipes_to_show', 'category_being_shown') do
page.replace_html("category_being_shown",
"Recipies For: #{@category.capitalize}")
page.replace_html("recipes_to_show", :partial => "recipes")
end
If you’re feeling more adventurous, you can patch the crossfade method directly into the JavaScriptHelper class and change the call to page.crossfade, making it more consistent with the other calls performed in an RJS template.
Testing RJS
Now for the Agile portion — how do you test this thing? RJS templates can be tested pretty thoroughly based on the content of the JavaScript code being generated. It’s much harder at the moment to test based on the actual results of the JavaScript — for example, it’s hard to test that the DOM elements being referenced actually exist in the client page.
There are two mechanisms for testing RJS that I’ve found useful. The first is mock object testing. Using the flexmock package to set up the page as a mock object, a sample test for the crossfade method looks like this:
def test_crossfade
page = flexmock("page")
page.should_receive(:visual_effect).with(:fade, "dom_1", :duration => 0.5).once.ordered(:first)
page.should_receive(:visual_effect).with(:fade, "dom_2", :duration => 0.5).once.ordered(:first)
page.should_receive(:delay).and_yield.once.ordered
page.should_receive(:replace_html).once.ordered
page.should_receive(:visual_effect).with(:appear, "dom_1", :duration => 0.5).once.ordered(:last)
page.should_receive(:visual_effect).with(:appear, "dom_2", :duration => 0.5).once.ordered(:last)
crossfade(page, "dom_1", "dom_2") do
page.replace_html()
end
end
Without going into the details of flexmock because, hey, future blog post, the idea is that the mocked page object keeps track of the method calls it receives and checks them against an expected set of method calls. In this case, I’m telling the mock object that it should get two calls to the visual_effect method with various arguments, and that those calls should come before the other calls.
The interesting thing about this method is that on the face of it, it contains no assertions. Implicitly, though, each should_receive call sets up an assertion about the messages coming to the object that is validated at the end of the test.
The other mechanism uses a plugin called ARTS (Another RJS Testing System), which defines a method called assert_rjs, which checks the outgoing JavaScript for a method call matching a set of parameters.
Using ARTS, the following method tests whether an individual DOM element is in the crossfade:
def assert_crossfade(dom_id, replacement)
assert_rjs :visual_effect, :fade, dom_id, :duration => 0.5
assert_rjs :visual_effect, :appear, dom_id, :duration => 0.5
assert_rjs :replace_html, dom_id, replacement
end
And a full test of the example would look like this:
def test_one_crossfade
assert_crossfade 'recipes_to_show', /Recipe/
assert_crossfade 'category_being_shown', /Category/
end
The replacement argument can be either a string, in which case the output needs to match exactly, or a regular expression, in which case a Regex match is performed. In general, assert_rjs works by recreating what the JavaScript looks like from the assert call, and checking the actual output for the existence of that call. Again, this is most helpful when you can bundle calls together in a single assertion.
ARTS is a nice little plugin, and it does let you do some syntactic testing of your RJS templates, but semantic testing along the lines of, is the recipes_to_show element visible at the end of the RJS and what text is actually in the element is still elusive. I think that’s doable, but it would require a much more complex mock object representing the page, one that keeps track of a pseduo-DOM tree and can manage at least some of the effects of RJS calls. Another project for another day…







