Guide: Test Driven RJS with ARTS

Posted by kev Mon, 29 May 2006 17:51:00 GMT

RJS is really a pain to debug. When things aren’t working right, they often don’t show up on the page at all. These truths make testing RJS especially important.

When researching for this article, I looked into the rjs_assertions plugin, but was generally unhappy with the offerings. rjs_assertions isn’t complete and the syntax felt cryptic.

So, I rolled my own. I’m calling the plugin ARTS: Another RJS Testing System, and the number one goal is to make an easily understandable syntax for testing RJS templates.

This guide is going to be broken into two parts: the installation and usage of the plugin, and using the plugin to test drive ajax interactions.

This isn’t an introduction to RJS. For background reading on the topic, see Rails RJS Templates by Cody Fauser and Rick Olson’s post on Rails Weenie.

Installing ARTS

ARTS is a plugin, so to install it:

  1. Go to your RAILS_ROOT directory
  2. Run script/plugin discover
  3. Run script/plugin install arts

To make use of the plugin, include the Arts module in your test class or in your test/test_helper.rb:

class Test::Unit::TestCase
  # ...

  # Add more helper methods to be used by all tests here...
  include Arts
end

Basic Usage

The ARTS plugin gives access to an assert_rjs method in your tests, which takes the name of a method you would call in an update_page block (like page.show ...) and the arguments you would pass to that method which it then uses to determine if the specified javascript was generated. For example, to assert that the javascript returned hides elements with the ids “post_1”, “post_2”, and “post_3”, the code would look like this:

assert_rjs :hide, "post_1", "post_2", "post_3"

Assertions for all methods described in here appear to work, but delay is untested until I figure out how the syntax should look.

Special Cases: insert_html, replace_html, and replace

The render syntax which the page.* methods use in RJS can not generate the content for assert_rjs. Because of this, content is optional, and when specified should be a string.

Update: These methods now support regular expression content matching.

# Checks that html is inserted before 'some_div'
assert_rjs :insert_html, :before, 'some_div' 

# Assert that "Some information" is inserted after the 'a_list' element.
assert_rjs :insert_html, :after, 'a_list', "Some information"

Test Driven RJS

Lets add ajax posting to a simple blog as an example. You can grab the source here if you’d like to look at the completed example closer. I’m going to assume we already have a layout setup which includes the Prototype and Scriptaculous libraries and have a working new/create cycle without ajax.

Our create action only takes in parameters, tries to save the post, and then either re-renders the new action or redirects.

def create
  @post = Post.new(params[:post])
  if @post.save
    redirect_to :action => 'show', :id => @post.id
  else
    render :action => 'new'
  end
end

We’d like the create action to actually add the new post to our page when it saves correctly instead of redirecting to the show action. We’ll start with writing a test.

Specifically, when a post is saved, we want to add the new post to the ‘posts’ div (in our layout) and let’s highlight it for good measure.

In this test, we’ll use the xhr method which simulates an ajax call. xhr takes a method (get or post) and then the normal get and post options.

We will also use our assert_rjs method from the Arts module to test:

def test_create_rjs
  xhr :post, :create, :post => {:title => "Yet Another Post", :body => "This is yet another post"}
  assert_rjs :insert_html, :bottom, 'posts'
  assert_rjs :visual_effect, :highlight, "post_#{assigns(:post).id}"    
end

If we take this test line by line, we first see the xhr call we described. It passes in title and body for the post. Next, we see a call to assert_rjs :insert_html. The call says that html is to be inserted at the bottom of the ‘posts’ div. Notice that it does not specify content after the ‘posts’ parameter, so we’re simply looking for whether an insertion occurred, not what is being inserted. Finally, there is a call to assert_rjs :visual_effect which checks for a highlight on the div of our newly saved and inserted post. For more about why this becomes the div id, see the partial.

So, now we run rake and get a big ugly error:

test_create_rjs(BlogControllerTest):
ActionController::MissingTemplate: Missing template /Users/kev/code/projects/arts_demo/config/../app/views/blog/create.rhtml

Right, we forgot to create our create.rjs file. Add an empty template and run again to see the next error message:

test_create_rjs(BlogControllerTest)
    [/Users/kev/code/projects/arts_demo/config/../vendor/plugins/arts/lib/arts.rb:35:in assert_rjs_insert_html'
...
<""> expected to be =~
</new Insertion.Bottom(.*posts.*,.*?);/>.

My error messages could be better (and should be improved in the future), but we can see that the assert_rjs :insert_html call has failed. This is because our template is blank. We’ll remedy that. create.rjs becomes:

page.insert_html :bottom, 'posts', :partial => 'post', :locals => {:post => @post}

This call says that we’re going to insert html at the bottom of the ‘posts’ div, and the content will be the result of running the ‘post’ partial passing in our new post.

If we run rake again we find that the assert_rjs :insert_html call passes and we’re left with the failure of the :visual_effect:

1) Failure:
test_create_rjs(BlogControllerTest)
visual_effect with args [highlight post_3] does not show up in response:
new Insertion.Bottom("posts", "<div id=\"post_3\">\n  <h2>Yet Another Post</h2>\n  <p>This is yet another post</p>\n</div>");.

We can see from the response that the visual_effect isn’t being rendered. This is because we haven’t added it to our template. If we add it, create.rjs looks like this:

page.insert_html :bottom, 'posts', :partial => 'post', :locals => {:post => @post}
page.visual_effect :highlight, "post_#{@post.id}"

So now, we should have the new post being inserted at the bottom of our ‘posts’ div and it should be highlighted.

If we run rake, the tests now pass. We know that the action will properly send back javascript. The only thing left is to change our form to use form_remote_tag. See the example for details. You may also want to be able to do both ajax and regular post interactions. This can be accomplished with respond_to but is outside the scope of this guide.

RJS is tough to debug without a bit of help. I hope that ARTS can fill that need. For further examples, see the tests of the example application. In test_create_rjs I include an example of assert_rjs :insert_html with “content included” and in test_bad_create_rjs a test for reporting of error messages. If you have any issues with ARTS, please feel free to contact me at kevin dot clark at gmail dot com.

This guide has been part of my weekly guide series which is released each Monday. If you have a conceptual question or an idea for a guide, please contact me at kevin dot clark at gmail dot com and include “[idea]” in the subject line.

Posted in , ,  | 25 comments

Comments

  1. Avatar Daniel said about 20 hours later:

    This is a great post. Thankyou for posting this one and writing a plugin for this aspect of testing. I look forward to adding this to my project to make it as robust as possible.

    Thanx

  2. Avatar brasten said about 20 hours later:

    You’ve managed to address many of the shortcomings I had with rjs-assertions. Very well done, and thanks for the useful tool!

  3. Avatar Tom said about 23 hours later:

    Will this test harness catch errors like, for example, a case when someone’s updating a div that doesn’t exist? Perhaps a designer renamed a div, or that div isn’t present on the page because it was moved to a seperate page in the UI?

    Seems like errors like this would be hard to test without actually loading up the DOM.

  4. Avatar Kevin Clark said about 23 hours later:

    Tom: It won’t catch errors in the DOM. If you know a way of simulating the way different browsers deal with the DOM in Ruby, please tell me and I’ll take a look :) As far as I know, you’d need a javascript compiler for that sort of thing.

    This is intended to assert that the expected div is being set to be updated. It’d be up to you to make sure the div is there. You might be able to use something like Selenium or Watir for the front end testing.

  5. Avatar rick said 1 day later:

    Hey Kevin, this is pretty slick. Unfortunately, I use a lot of inline element calls that it’s not catching.

    # $('foo').update('blah')
    # but the plugin looks for
    # Element.update('foo', 'blah')
    page[:foo].replace_html 'blah'

    Also, this could use some sanity tests :)

    I’ll hack on this a bit tonight and see if I can provide a patch.

  6. Avatar Kevin Clark said 1 day later:

    Hey Rick, Yeah, I hacked this up in a hurry so I could write the article. It needs tests some tweaks. I should have a patch for regexes on content soon.

  7. Avatar seb said 2 days later:

    Hi

    I have installed your plugin and it works great. Thanks a lot.

    Just a weird thing, now if I run “rake load_fixtures” here is the output:

    Loaded suite /opt/local/bin/rake

    Started

    Finished in 0.000392 seconds.

    0 tests, 0 assertions, 0 failures, 0 errors

    It tries to run some dummy tests. Have you notice that?

  8. Avatar Kevin Clark said 2 days later:

    seb: This is happening because the arts init.rb file automatically includes the module in Test::Unit::TestCase, which on second thought doesn’t need to happen. I say in the README (and in this guide) to include the module manually in test_helper. The dummy tests should come out in the next revision.

    Thanks for the heads up!

  9. Avatar seb said 3 days later:

    thanks for the quick answer:

    I tried to test a visual_effect like this
    assert_rjs :visual_effect, :blind_up, "line_#{id}" 
    
    Here is the controller code:
    page["line_#{params[:id]}"].visual_effect :blind_up
    
    But it does not work because the controller generates:
    .$("line_2").visualEffect("blind_up");
    
    and the test tries to compare this with:
    new Effect.BlindUp("line_2",{});
    
    Did I misunderstood something ?
  10. Avatar Kevin Clark said 3 days later:

    seb: Currently arts doesn’t work with the selector syntax (page[‘blah’].blind_up) because it outputs different code than the normal page.visual_effect :blind_up stuff. It’s something I’d like to add in the very near future so keep on the lookout. I’ll mention it on the blog.

  11. Avatar joe goldberg said 7 days later:

    Hi Kevin,

    After installing ARTS, I’m seeing debug statements appear on my pages along with AJAX content. Eg:

    “Loaded suite /usr/local/depot/coffeerobot/trunk/public/dispatch.cgi Started Finished in 0.000269 seconds. 0 tests, 0 assertions, 0 failures, 0 errors”

    Users of the file_column plugin have seen a similar issue:

    http://www.sitepoint.com/forums/showthread.php?p=2671883

    But I couldn’t find anything analogous to the fix described in that thread for ARTS.

    Any clue what’s going on?

    Thanks, Joe Goldberg Jobster.com, SDE

  12. Avatar Kevin Clark said 7 days later:

    Hi Joe, Those mesages show up whenever ‘test/unit’ is included, whether you run test cases or not. Since we’ve chosen to automatically require ‘test/unit’ for you on runtime to make it easy to work with you’ll see those here and there. They won’t cause problems. I you’d rather not see them, you can go into plugins/arts/init.rb and remove the line, and then ‘include Arts’ in your test_helper.rb inside the Test::Unit::TestCase class.

  13. Avatar Zack Chandler said 7 days later:

    Nice work. One improvement idea is to handle:

    page.replace_html 'post-errors', error_messages_for(:post)
    assert_rjs :replace_html, 'post-errors', error_messages_for(:post)

    I notice that right now this doesn’t seem to work.

    Again, nice job!

  14. Avatar Kevin Clark said 7 days later:

    Oh, those shouldn’t show up on your pages, just in the console.. are you redirecting stdout to the page?

  15. Avatar Kevin Clark said 7 days later:

    Zack: To get that working, you’d need to include the ActionView helper which constructs error_messages, and probably have to override it to use assigns(‘post’) instead of the instance variable post.

  16. Avatar Zack Chandler said 9 days later:

    Kevin,

    Using assigns worked like a charm :)

    Thanks a ton – I’m really finding this useful.

    P.S. In the article you say:

    To make use of the plugin, include the Arts module in your test class or in your test/test_helper.rb:

    class Test::Unit::TestCase
      # ...
    
      # Add more helper methods to be used by all tests here...
      include Arts
    end

    This isn’t necessary as you already include it in init.rb

      Test::Unit::TestCase.send :include, Arts

    Thanks again, Zack

  17. Avatar Kevin Clark said 9 days later:

    Zack: At the time it was written I wasn’t including the module in Test::Unit::TestCase so you did need to include. If you see the plugin’s README, it now doesn’t instruct you to add the include.

  18. Avatar Zack Chandler said 9 days later:

    Got it. One more quick question. Is there a way to test insert_html calls that render partials? render() is a protected method of ActionController::Base so I’m not sure how this method could be accessible elsewhere.

    Ex:

    rjs
      page.insert_html :bottom, 'my_list', render(:partial => 'my_list_item', :object => @list_item)
    test?
      assert_rjs :insert_html, :bottom, 'my_list', render...???

    Thanks, Zack

  19. Avatar Kevin Clark said 10 days later:

    Zack: Rendering partials in tests is problematic, so I don’t have a way to test for this well yet. You can use the content matching to explicitly look for a match, but probably the better solution will be to use regular expression content matching which should exist soonish.

  20. Avatar Zack Chandler said 10 days later:

    Kevin,

    I agree. With regex you can at least check that the content contains a bit of what you expect and that will probably catch as many bugs as an exact match.

    I’ll look forward to it.

    Zack

  21. Avatar vijayramanan said 13 days later:

    I initially started off by using rjsassertions http://ibrasten.com/articles/2006/04/05/rjs-assertions

    Most of my assertions were failing. No offence to anybody.. maybe I was not using it properly. But I noticed that your syntax is so simple and neat and almost translates what I have done on the inline rjs in my controller. I love this thanks a lot for writing this neat plugin. Iam also interested in some addons like proper session assertions (assert_session_has) currently the defualt assertion only checks for key . If you could add on a feature here which could asset the content inside session that will be cool :) . Since most of the remote calls require session for state maintenance .. just a thought..

  22. Avatar Chetan said 15 days later:

    Very very slick and useful. Thx!

    Here is a patch for the replace_html issue mentioned in Comment #5

    vendor/plugins/arts/lib/arts.rb

    52,60c52,60 < when Regexp < assert_match /(\$\([’”]#{div}[’”]\).update\([’”]|Element.update\(.#{div}.,[’”]).?\);/, #’ < @response.body < when String < assert (lined_response.include?(“Element.update(\”#{div}\”, #{content});”) || lined_response.include?(%Q!$(”#{div}”).update(”#{content}”);.!)), #” < “No replace_html call found on div: ’#{div}’ and content: \n#{content}\n” + < “in response:\n#{lined_response}” < else < raise “Invalid content type” - > when Regexp > assert_match Regexp.new(“Element.update(.#{div}.,.#{content.source}.);”), > @response.body > when String > assert lined_response.include?(“Element.update(\”#{div}\”, #{content});”), > “No replace_html call found on div: ’#{div}’ and content: \n#{content}\n” + > “in response:\n#{lined_response}” > else > raise “Invalid content type” 63,64c63 < # assert_match Regexp.new(“Element.update(.#{div}.,.?);”), @response.body, “Regexp for replace_html didn’t match” < assert_match /(\$\([’”]#{div}[’”]\).update\([’”]|Element.update\(.#{div}.,[’”]).?\);/, @response.body - > assert_match Regexp.new(“Element.update(.#{div}.,.*?);”), @response.body

    I hope the formatting of the diff comes out ok. I have done very light testing, so user beware.

  23. Avatar Chetan said 15 days later:

    Sorry, here is a better formatted version of the earlier post.

    A patch for the replace_html issue mentioned in Comment #5

    vendor/plugins/arts/lib/arts.rb

    52,60c52,60
    &lt;       when Regexp
    &lt;         assert_match /(\$\(['"]#{div}['"]\).update\(['"]|Element.update\(.*#{div}.*,['"]).*?\);/, #'
    &lt;             @response.body
    &lt;       when String
    &lt;         assert (lined_response.include?("Element.update(\"#{div}\", #{content});") || lined_response.include?(%Q!$("#{div}").update("#{content}");.*!)), #" 
    &lt;         "No replace_html call found on div: '#{div}' and content: \n#{content}\n" +
    &lt;                       "in response:\n#{lined_response}" 
    &lt;       else
    &lt;         raise "Invalid content type" 
    ---
    >         when Regexp
    >           assert_match Regexp.new("Element.update(.*#{div}.*,.*#{content.source}.*);"),
    >                        @response.body
    >         when String
    >           assert lined_response.include?("Element.update(\"#{div}\", #{content});"), 
    >                  "No replace_html call found on div: '#{div}' and content: \n#{content}\n" +
    >                  "in response:\n#{lined_response}" 
    >         else
    >           raise "Invalid content type" 
    63,64c63
    &lt;       # assert_match Regexp.new("Element.update(.*#{div}.*,.*?);"), @response.body, "Regexp for replace_html didn't match" 
    &lt;       assert_match /(\$\(['"]#{div}['"]\).update\(['"]|Element.update\(.*#{div}.*,['"]).*?\);/, @response.body
    ---
    >       assert_match Regexp.new("Element.update(.*#{div}.*,.*?);"), @response.body
    

    I have done very light testing, so user beware.

  24. Avatar Kevin Clark said 15 days later:

    Chetan: Cool stuff, thanks. I’m really looking for a more general solution so that any method that can work like page[‘some_id’].hide will be supported, but your stuff may work in the interim.

  25. Avatar Chetan said 16 days later:
    Hi Kevin, yeah, I was stuck after downloading your plugin on the replace_html issue; so quickly hacked up a fix. A cleaner solution is definitely desirable. BTW, found a bug with the regexp when content is specified in my prev diff (I am still new to ruby). Here is the new patch.
    53,54c53,54
    &lt;         assert_match /(\$\(['"]#{div}['"]\).update\(|Element.update\(.*#{div}.*,)['"].*#{content.source}.*['"]\);/, #'
    &lt;         @response.body, "No match for regexp\n($\(['\"]#{div}['\"]\).update\(|Element.update\(.*#{div}.*,)['\"].*#{content.source}.*['\"]\);\nin response.body\n#{@response.body}" 
    ---
    >         assert_match /(\$\(['"]#{div}['"]\).update\(['"]|Element.update\(.*#{div}.*,['"]).*?\);/, #'
    >             @response.body
    56,58c56,58
    &lt;         assert (lined_response.include?("Element.update(\"#{div}\", #{content});") || lined_response.include?(%Q!$("#{div}").update("#{content}");!)), #" 
    &lt;         "No replace_html call found on div: '#{div}' and content: #{content} matching either\n" + "Element.update(\"#{div}\", #{content});" + "\nor\n" + 
    &lt;                       %Q!$("#{div}").update(#{content});! + "\nin response:\n#{lined_response}\n#{lined_response.class}" 
    ---
    >         assert (lined_response.include?("Element.update(\"#{div}\", #{content});") || lined_response.include?(%Q!$("#{div}").update("#{content}");.*!)), #" 
    >         "No replace_html call found on div: '#{div}' and content: \n#{content}\n" +
    >                       "in response:\n#{lined_response}" 
    
    Hope it is useful

Comments are disabled