3

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?

rctneil
  • 7,016
  • 10
  • 40
  • 83
  • It's a boolean field. I'll post all the :on_ride_photo specs in my OP. – rctneil Sep 08 '13 at 14:49
  • There's been a lot of activity on this question. Please let Billy and/or me know if our answers have been helpful/acceptable or if you have any follow-up comments or questions. Thanks. – Peter Alfvin Sep 10 '13 at 14:00

2 Answers2

2

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.

Community
  • 1
  • 1
Peter Alfvin
  • 28,599
  • 8
  • 68
  • 106
  • Ok, That makes sense. When using that though, these two specs fail: it { should_not allow_value(4).for(:on_ride_photo) } it { should_not allow_value('yes').for(:on_ride_photo) }` – rctneil Sep 08 '13 at 15:58
  • 1
    So it's saying that 4 and 'yes' are both classed as true and should be allowed but the test is failing as they are being allowed when the test is trying to make sure that they fail? Is there a way around this? – rctneil Sep 08 '13 at 16:10
  • Yes, that's right. I'm not sure what you mean by "way around this", but I would think of the `allow_value` method in terms of what the attribute allows as _input_, in which case both 4 and 'yes' are allowed and your example should reflect that. – Peter Alfvin Sep 08 '13 at 19:00
  • Hmm, I ONLY want to accept true and false, 4 and 'yes' should fail - This is how I want it to work and just am totally unsure of how I can get that result. At the moment, if someone enters in 4 then it will turn into a true value and I don't want that. This is where I am stuck. – rctneil Sep 08 '13 at 19:04
  • It's a PostgreSQL database – rctneil Sep 08 '13 at 20:34
  • @PeterAlfvin, I've made some tests about that, check my answer. – Billy Chan Sep 09 '13 at 07:23
1

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.

Billy Chan
  • 24,625
  • 4
  • 52
  • 68
  • Good work finding the source code, Billy. Not only did in uncover `true` values I missed, but it pointed out that "" was treated as nil. I'll delete the latter part of my answer and refer to yours. Still wondering how to use validations to reject non-intuitive input values, though (e.g. anything not in `TRUE_VALUES` and not in `FALSE_VALUES`). – Peter Alfvin Sep 09 '13 at 16:02
  • @PeterAlfvin, I think rejecting non-intuitive values is possible, by check if value is in a concat of this two constants, in similar fashion of `inclusion: {in: combined_enum }`. But I think that would be a little bit of overkill :) – Billy Chan Sep 09 '13 at 16:29
  • Given this whole thread, I think `inclusion: {in: ...}}` is checking after the coercion occurs. If you want to reject any non-empty values, you'll need to do it some other way. – Peter Alfvin Sep 09 '13 at 20:04