rSpec Shoulda matchers testing validations

3.1k Views Asked by At

I have the following validation on an attribute of a model:

validates :on_ride_photo,
  presence: true,
  inclusion: { in: [true, false] }

I then have the following tests:

context 'on_ride_photo' do
  it { should validate_presence_of(:on_ride_photo) }
  it { should allow_value(true).for(:on_ride_photo) }
  it { should allow_value(false).for(:on_ride_photo) }
  it { should_not allow_value(nil).for(:on_ride_photo) }
  it { should_not allow_value(4).for(:on_ride_photo) }
  it { should_not allow_value('yes').for(:on_ride_photo) }
end

But I get the following error when running my specs:

2) Coaster validations should allow on_ride_photo to be set to false
   Failure/Error: it { should allow_value(false).for(:on_ride_photo) }
     Did not expect errors  when on_ride_photo is set to false, got error:
   # ./spec/models/coaster_spec.rb:86:in `block (3 levels) in <top (required)>'

Is the fact that I want to allow false as a valid value being knocked down by the fact it has to be present and that false is classed as not present? If so, then how can I work around this?

2

There are 2 best solutions below

6
On

Yes, the issue is with presence: true, which checks that the attribute is not Object#blank?. Since the validations are additive, the inclusion: clause can't "override" the fact that false fails the presence test.

The following is from http://edgeguides.rubyonrails.org/active_record_validations.html#presence:

Since false.blank? is true, if you want to validate the presence of a boolean field you should use validates :field_name, inclusion: { in: [true, false] }.

See Billy Chan's answer for a discussion of how boolean fields are handled, including in particular information on which values are treated as nil, true and false.

3
On

Finally I got the answer, thanks to Peter's updating.

  1. To set up boolean value in migration, use t.boolean :foo
  2. To validate boolean value, use the validator recommended in Peter's answer.
  3. To know what values will work as true or false value, see below. (I don't think it's necessary to unit test them though integration or functional tests for including parts of them would be okay)

I just browsed the source code. The type boolean, either PostgreSQL, Sqlite3 or MySQL will finally point to ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value),

    # convert something to a boolean
    def value_to_boolean(value)
      if value.is_a?(String) && value.empty?
        nil
      else
        TRUE_VALUES.include?(value)
      end
    end

and then got judged by this constant

  # the namespace is still "ActiveRecord::ConnectionAdapters::Column"
  TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON'].to_set
  FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set

So, we finally got it.

  1. If empty, nil

  2. If not empty, check if it is in TRUE_VALUE, if not, false. That's why "4", "yes" is false.