rails error wrapping for select input fields of referenced models

Sunday, September 16th, 2007

during my rails development i recently came across the problem mentioned here. this is my concrete scenario…
first of all there are two model elements referenced by belongs_to resp. has_many:

class WorkingTime < ActiveRecord::Base

  belongs_to  :project

  validates_presence_of :project, :message => 'Please select a project!'
  validates_associated :project
  validates_presence_of :hours_worked

end

class Project < ActiveRecord::Base
  has_many :working_times
end

the input form to create an instance of this model looks something like this:

[...]
<%= error_messages_for 'working_time' %>

<label for="project_of_working_time">Project</label>
<%= select :working_time, :project_id,
    Project.find(:all).collect {|p| [ p.name, p.id ] },
    { :include_blank => true }, :class => "working_time_select" %>

<label for="working_time_hours_and_minutes_worked">Hours Worked</label>
<%= text_field 'working_time', 'hours_worked', :size => 5  %>

[...]

now if you try to save the form without selecting a project an appropriate error message is shown. this is not really remarkable. but the problem is that the select input field isn’t highlighted correctly like other input fields (i.e. the hours_worked if it is empty). i had a hard time to figure out the reason for this.

the error highlighting is based on the errors object that is passed to the view in case of validation errors. when the view is evaluated every input field looks in this object for an error associated to it by its name. if the method errors.on(name of input field) returns a value, a html-snipped is wrapped around the input field (btw, if you want to change the snipped, look here). here is an example for the input textfield hours_worked:

<div class="fieldWithErrors">
<input id="workind_time_hours_worked"
     name="working_time[hours_worked]" size="5" type="text" value="" />
</div>

the issue with the above mentioned working_time / project relation is that the validation error is mapped to the field project and not to project_id, since this is what i described in the model working_time. because the view cannot get the link to the project-error, the field will not be highlighted. you can validate this with a unit-test:

require 'working_time'

class WorkingTimeTest < Test::Unit::TestCase

  def test_create_without_project
    @working_time = WorkingTime.new
    @working_time.hours_worked = 3
    result = @working_time.save
    assert !result
    errors = @working_time.errors
    assert_not_nil errors[:project]
    assert includes_match?(errors[:project], 'Please select a project!')
  end
end

the only solution is to change the validation in the working_time model from project to project_id. this is quite confusing because the working_time model actually doesn’t have a project_id field. what makes this even worse is that you also get validation errors if a project instance will be set in the working_time model. therefore the following test always fails:

require 'working_time'
require 'project'

class WorkingTimeTest < Test::Unit::TestCase

  def test_create_with_project
    @working_time = WorkingTime.new
    @working_time.hours_worked = 3
    @working_time.project = Project.find(1)
    result = @working_time.save
    assert result
  end
end

i hope this issue will be fixed in a future version of rails.

Tags: , ,

how to test view helpers in rails

Thursday, August 9th, 2007

as you probaby know, ruby on rails has a very handy test integration. you can easily write unit tests for your rich domain model and some functional tests (that can be roughly compared to http-units in the java world, but with integrated webserver) for the controller and views.

unfortunately, one part of rails basic implementation architecture cannot be tested as easy as the above mentioned components: the view helpers. actually they are used to extract view functionality and keep the view templates clean from ruby scriptlets. of course, you can test them indirectly through your functional tests, but rails provides no pure way to test them individually. an obvious approach is to write some unit-tests for each helper class. this works fine as long as you are not using some methods provided by rails, i.e. link_to, url_for, all the input tag methods and so on.

so here is an example of a simple view helper. the method time_link_to returns a link including the current date as parameter in the form http://www.yourhost.com/railscontroller/year/month/day.

# Methods added to this helper will be
# available to all templates in the application.
module ApplicationHelper

  def time_link_to(controller_name, name, date, html_options = nil)
    if html_options
      html_options = html_options.stringify_keys
      convert_options_to_javascript!(html_options)
      tag_options = tag_options(html_options)
    else
      tag_options = nil
    end
    url = url_for(:controller => controller_name) +
           "/#{date.year}/#{date.month}/#{date.day}"
    "<a href=\"#{url}\"#{tag_options}>#{name || url}</a>"
  end

end

since the method url_for is provided by the controller that is executed when the view and therefore the helper is evaluated, you have to fake it for the unit test. this can be done in the setup method of the unit test like this:

class ApplicationHelperTest < Test::Unit::TestCase

  include ActionView::Helpers::AssetTagHelper
  include ActionView::Helpers::UrlHelper
  include ActionView::Helpers::TagHelper
  include ApplicationHelper

  # called before every single test
  def setup
    # creating a dummy controller with the method url_for that
    # returns the url assigned to the controller
    @controller = Class.new do
      attr_accessor :url, :request
      def url_for(options, *parameters_for_method_reference)
        url
      end
    end
    @controller = @controller.new
    # setting the url of the controller
    @controller.url = "http://www.example.com"
  end

  # testing the view method
  def test_time_link_to
    assert_equal "<a href=\"http://www.example.com/testcontroller/" +
      "#{Date.today.year}/#{Date.today.month}/" +
      "#{Date.today.day}\">test</a>",
      time_link_to("testcontroller", "test", Date.today)
    assert_equal "<a href=\"http://www.example.com/testcontroller/" +
      "#{Date.today.year}/#{Date.today.month}/" +
      "#{Date.today.day}\" id=\"test_id\">test</a>",
      time_link_to("testcontroller", "test",
        Date.today, {:id => "test_id"})
  end
end
Tags: , , , , ,