Saturday, November 17, 2012

Rails 3: Improve :presence validation

In Rails 3.0 (and probably in modern versions) you can ensure the validity of your models by using validations . One I use is the :presence , to make an object invalid if a variable is not present.

Example:

class Car < ActiveRecord::Base
  belongs_to :user
  validates :user, :presence => true
  ...
end

The problem comes when you investigate how validation is made. Each time we save the validated object is got from the database (*). Something simple as:


car = Car.last
car.save

will hit the database checking if user exists.

Solution: build a custom Validator and use it:


class SexyForeignKeyValidator < ActiveModel::EachValidator
  #http://guides.rubyonrails.org/active_record_validations_callbacks.html#performing-custom-validations
  def validate_each(record, attribute, value)
     return if value.present?
     relation_name = attribute.to_s.gsub(/_id$/,'')
     unless record.send(relation_name)
        record.errors.add relation_name.to_sym, :blank
     end
  end
end

The tricky part is to check for the foreign_key BUT if this is not already set (on create), check for the relation.
This validation run on the foreign key, so we modify the code in the model.


class Car < ActiveRecord::Base
  belongs_to :user
  validates :user_id, :sexy_foreign_key => true
  ...
end

One last note. The code for the validator must be included in some path that is automatically loaded ( read this)

Profit!


(*) Note that this is a very particular case, when we check the presence of a relation. Checking for the foreign_key user_id won't work, since on creation, this foreign_key can be null

Friday, November 2, 2012

Test the HTML sent from rails

Sending emails in Rails is a piece of cake when you follow the official mailers guide . However testing the html is a bit problematic.

You can use the assert_select_email or the usual assert_select methods. I prefer the second option, and my code looks like (Rails 3.0.X)

email = ActionMailer::Base.deliveries.last
body = email.html_part.decoded
 
node = HTML::Document.new(body).root
assert_select node, ':root' do
   #do your assertions here
end
 
It took me, however, some time to fix the error
NoMethodError: undefined method `assert_select' for

In Rails 3.0.X it is as easy as including the appropriate module (within you test class)  

include ActionDispatch::Assertions::SelectorAssertions 


For newer rails versions check the official guide of teh the assert_select method, and check which module is it defined in.