Using url_for in Ruby on Rails tests

Posted by Ben on May 04, 2007

On a recent Ruby on Rails project, we ran into a situation where we needed to use the ActionController::Base url_for method in our tests. My first attempt was to simply write the test as follows:

def test_next__should_show_step3
  @request.env["HTTP_REFERER"] = @controller.url_for(
    :controller => 'wizard',
    :action => 'step',
    :step_number => "1",
    :id => 1)
 
  post :next, :id => 1
  assert_not_nil
  assigns(:virtual_tour)
  assert_response :redirect
  assert_redirected_to :action => 'step', :step_number => "3", :id => assigns(:virtual_tour).id
end

When I ran the test it resulted in the following error:

test_next__should_show_step3(VirtualTourWizardControllerTest):
NoMethodError: You have a nil object when you didn't expect it! The error occured while evaluating nil.rewrite
/usr/local/lib/ruby/gems/1.8/gems/actionpack-1.12.5/lib/action_controller/base.rb:488:in `url_for'
./test/functional/virtual_tour_wizard_controller_test.rb:91:in `set_referer_step'
./test/functional/virtual_tour_wizard_controller_test.rb:64:in `test_next__should_update_virtual_tour'

When we looked at the rails code, we saw that an instance variable @url was not initialized yet in our tests. This was causing the nil error seen above. We found a post that wrote about a work around by initializing this variable by calling get :index or some other action that doesn’t cause a side effect. This seems like a bit of a hack so we dug a little further.

After a little digging in the rails source code, we found the class that does the actual URL rewriting. The class is called ActionController::UrlRewriter which url_for delegates to to do the actual URL rewriting. So we created a little helper method in test_helper.rb to wrap ActionController::UrlRewriter in a nice helper method called url_for. The following is the method we created in test_helper.rb

def url_for(options)
  url = ActionController::UrlRewriter.new(@request, nil)
  url.rewrite(options)
end

We can know rewrite our test to look like this.

def test_next__should_show_step3
  @request.env["HTTP_REFERER"] = url_for(
    :controller => 'wizard',
    :action => 'step',
    :step_number => "1",
    :id => 1)
 
  post :next, :id => 1
  assert_not_nil
  assigns(:virtual_tour)
  assert_response :redirect
  assert_redirected_to :action => 'step', :step_number => "3", :id => assigns(:virtual_tour).id
end

We ended up doing a little more refactoring after this since we set the referer like this in many of our other tests.

Trackbacks

Use this link to trackback from your own site.

Comments

Leave a response

  1. justin Tue, 29 May 2007 13:34:38 PDT

    thanks a lot, very helpful. I added this to my test_helper

    def url_for options
    url = ActionController::UrlRewriter.new(@request, nil)
    # rewrite the url, but we don’t want the protocol and host part, only the actual path
    url.rewrite(options.merge( :protocol => ‘’, :host => ‘’ ))
    end unless defined? url_for

    I erase the protocol and host so that the returned path does NOT have the http://example.com/ part, which gets in the way.

    May not be the best way to do it, but seems to fill me needs.

  2. justin Tue, 29 May 2007 13:36:26 PDT

    actually, merging in :o nly_path => true is a better solution than overwriting the protocol and host parts.

  3. Ben Tue, 29 May 2007 13:46:29 PDT

    Thanks Justin. We created the bare minimum here. If you want url_for to work exactly like ActionController::Base I suggest that you take a look at the ActionController::Base.url_for source code. There is a little more checking on whether the parameter is a String, Hash or Symbol. It also provides a way to provide default options.

    It is a shame that all of this behavior is not encapsulated. It may be a good patch to extract the url_for method into a helper class that ActionController::Base uses. We could then use the same helper class in our tests and ensure the functionality between ActionController::Base.url_for and the test_help.url_for remain identical.

    Maybe I’ll bring that up on the Ruby on Rails Core team mailing list to see if it would be worth my time.

Comments