Jan Dudek
Jan Dudek

Reputation: 807

How to test which validation failed in ActiveRecord?

I have a model like this:

class User < ActiveRecord::Base
  validates_length_of :name, :in => (2..5)
end

I want to test this validation:

it "should not allow too short name" do
  u = User.new(:name => "a")
  u.valid?
  u.should have(1).error_on(:name)
end

But then it does not test which kind of error was set on name. I want to know, if it was too_short, too_long, or maybe some other validation failed.

I can lookup the message text in errors array, like this:

u.errors[:name].should include(I18n.t("activerecord.errors.models.user.attributes.name.too_short"))

But this will fail when I set activerecord.errors.messages.too_short in locale file instead of model-specific message.

So, is it possible to check which kind of error occured?

Upvotes: 29

Views: 21704

Answers (4)

fny
fny

Reputation: 33537

Rails added an method to query for errors to ActiveModel in late 2011 and Rails v3.2. Just check to see if the appropriate error has been #added?:

# An error added manually
record.errors.add :name, :blank
record.errors.added? :name, :blank # => true

# An error added after validation
record.email = '[email protected]'
record.valid? # => false
record.errors.added? :email, :taken, value: '[email protected]' # => true

Note that for validations that are parametrized (e.g. :greater_than_or_equal_to) you'll need to pass the value of the parameter too.

record.errors.add(:age, :greater_than_or_equal_to, count: 1)
record.errors.added?(:age, :greater_than_or_equal_to, count: 1)

Errors are identified by their i18n key. You can find the appropriate keys to check in the appropriate Rails i18n file for any language under the error section.

Some other nifty questions you can ask ActiveModel#Error are #empty? and #include?(attr), as well as anything you can ask an Enumerable.

Upvotes: 43

Ben5e
Ben5e

Reputation: 721

The approach I use:

it "should not allow too short name" do
  u = User.new(:name => "a")
  expect{u.save!}.to raise_exception(/Name is too short/)
end

I use a regex to match the exception message because there may be many validation messages in the exception message, but we want to ensure that it contains a specific snippet relating to the name being too short.

This approach does couple your assertions to your validation messages, so if you every modify your validation message you'll likely need to modify your specs as well. Overall though, this is a simple way to assert validations are doing their job.

Upvotes: 5

Jan Dudek
Jan Dudek

Reputation: 807

I really don't like the idea of looking for translated error messages in Errors hash. After a conversation with a fellow Rubyists, I ended monkey patching Errors hash, so it saves the non-translated message first.

module ActiveModel
  class Errors
    def error_names
      @_error_names ||= { }
    end

    def add_with_save_names(attribute, message = nil, options = {})
      message ||= :invalid
      if message.is_a?(Proc)
        message = message.call
      end
      error_names[attribute] ||= []
      error_names[attribute] << message
      add_without_save_names(attribute, message, options)
    end

    alias_method_chain :add, :save_names
  end
end

Then you can test like this:

u = User.new(:name => "a")
u.valid?
u.errors.error_names[:name].should include(:too_short)

Upvotes: 12

Peter Brown
Peter Brown

Reputation: 51697

I recommend checking out the gem shoulda for handling these types of repetitive validation tests. It complements RSpec or Test::Unit so you can write concise specs such as:

describe User do
  it { should ensure_length_of(:name).is_at_least(2).is_at_most(5) }
end

Upvotes: 5

Related Questions