thoughtbot--shoulda-matchers/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb

163 lines
4.7 KiB
Ruby
Raw Normal View History

module Shoulda
2013-12-05 16:38:30 +00:00
module Matchers
module ActiveModel
# The `validate_absence_of` matcher tests the usage of the
# `validates_absence_of` validation.
#
# class PowerHungryCountry
# include ActiveModel::Model
# attr_accessor :nuclear_weapons
#
# validates_absence_of :nuclear_weapons
# end
#
# # RSpec
# RSpec.describe PowerHungryCountry, type: :model do
# it { should validate_absence_of(:nuclear_weapons) }
# end
#
# # Minitest (Shoulda)
# class PowerHungryCountryTest < ActiveSupport::TestCase
# should validate_absence_of(:nuclear_weapons)
# end
#
# #### Qualifiers
#
# ##### on
#
# Use `on` if your validation applies only under a certain context.
#
# class PowerHungryCountry
# include ActiveModel::Model
# attr_accessor :nuclear_weapons
#
# validates_absence_of :nuclear_weapons, on: :create
# end
#
# # RSpec
# RSpec.describe PowerHungryCountry, type: :model do
# it { should validate_absence_of(:nuclear_weapons).on(:create) }
# end
#
# # Minitest (Shoulda)
# class PowerHungryCountryTest < ActiveSupport::TestCase
# should validate_absence_of(:nuclear_weapons).on(:create)
# end
#
# ##### with_message
2013-12-05 16:38:30 +00:00
#
# Use `with_message` if you are using a custom validation message.
#
# class PowerHungryCountry
# include ActiveModel::Model
# attr_accessor :nuclear_weapons
#
# validates_absence_of :nuclear_weapons,
# message: "there shall be peace on Earth"
# end
#
# # RSpec
# RSpec.describe PowerHungryCountry, type: :model do
# it do
# should validate_absence_of(:nuclear_weapons).
# with_message("there shall be peace on Earth")
# end
# end
#
# # Minitest (Shoulda)
# class PowerHungryCountryTest < ActiveSupport::TestCase
# should validate_absence_of(:nuclear_weapons).
# with_message("there shall be peace on Earth")
# end
#
# @return [ValidateAbsenceOfMatcher}
2013-12-05 16:38:30 +00:00
#
def validate_absence_of(attr)
ValidateAbsenceOfMatcher.new(attr)
end
# @private
class ValidateAbsenceOfMatcher < ValidationMatcher
def initialize(attribute)
super
@expected_message = :present
2013-12-05 16:38:30 +00:00
end
def matches?(subject)
super(subject)
disallows_value_of(value, @expected_message)
end
Fix negative versions of validation matchers When using a validation matcher in the negative, i.e.: should_not validate_*(...) as opposed to: should validate_*(...) ...it's common to receive the following error: undefined method `attribute_setter' for nil:NilClass This happens particularly when using a matcher that makes use of AllowValueMatcher or DisallowValueMatcher internally (which all of the validation matchers do). Whenever you make an assertion by using a matcher in the negative as opposed to the positive, RSpec still calls the `matches?` method for that matcher; however, the assertion will pass if that returns *false* as opposed to true. In other words, it just inverts the result. However, whenever we are using AllowValueMatcher or DisallowValueMatcher, it doesn't really work to invert the result. like this. This is because AllowValueMatcher and DisallowValueMatcher, despite their name, aren't truly opposites of each other. AllowValueMatcher performs these steps: 1. Set the attribute on the record to some value 2. Run validations on the record 3. Ask whether validations pass or fail 4. If validations fail, store the value that caused the failure along with the validation errors and return false 5. Otherwise, return true However, DisallowValueMatcher performs these steps: 1. Set the attribute on the record to some value 2. Run validations on the record 3. Ask whether validations pass or fail 4. If validations *pass*, store the value that caused the failure along with some metadata and return false 5. Otherwise, return true This difference in logic is achieved by having AllowValueMatcher implement `does_not_match?` and then having DisallowValueMatcher use this for its positive case and use `matches?` for its negative case. It's easy to see because of this that `does_not_match?` is not the same as `!matches?` and vice versa. So a matcher that makes use of these submatchers internally needs to use their opposite versions whenever that matcher is used in the negative case. In other words, all of the matchers need a `does_not_match?` which is like `matches?`, except that all of the logic is inverted, and in all the cases in which AllowValueMatcher is used, DisallowValueMatcher needs to be used. Doing this ensures that when `failure_message` is called on AllowValueMatcher or DisallowValueMatcher, step 4 in the list of steps above stores a proper value that can then be referenced in the failure message for the validation matcher itself.
2018-09-08 20:59:24 +00:00
def does_not_match?(subject)
super(subject)
allows_value_of(value, @expected_message)
end
def simple_description
"validate that :#{@attribute} is empty/falsy"
2013-12-05 16:38:30 +00:00
end
private
def value
if reflection
obj = reflection.klass.new
if collection?
[obj]
2013-12-05 16:38:30 +00:00
else
obj
end
elsif array_column?
['an arbitrary value']
elsif enum_column?
enum_values.first
2013-12-05 16:38:30 +00:00
else
Tighten CouldNotSetAttributeError restriction Why: * Previously, `allow_value` would raise a CouldNotSetAttributeError if the value being set didn't match the value the attribute had after being set, but only if the attribute was being changed from nil to non-nil or non-nil to nil. * It turns out it doesn't matter which value you're trying to set the attribute to -- if the attribute rejects that change it's confusing either way. (In fact, I was recently bit by a case in which I was trying to validate numericality of an attribute, where the writer method for that attribute was overridden to ensure that the attribute always stored a number and never contained non-number characters. This ended up making the numericality validation useless, of course -- but it caused confusion because the test acted in a way I didn't expect.) To satisfy the above: * `allow_value` now raises a CouldNotSetAttributeError if the attribute rejects the value being set in *any* way. * However, add a `ignoring_interference_by_writer` qualifier so that it is possible to manually override this behavior. * Fix tests that are failing now because of this new change: * Fix tests for allow_value matcher * Fix tests for numericality matcher * Remove tests for numericality matcher + integer column * An integer column will typecast any non-integer value to an integer. * Because of the typecasting, our tests for the numericality matcher against an integer column don't quite work, because we can't really test what happens when the attribute is set to a non-integer value. Now that `allow_value` is more strict, we're getting a CouldNotSetAttributeError when attempting to do so. * The tests mentioned were originally added to ensure that we are handling RangeErrors that ActiveRecord used to emit. This doesn't happen anymore, so the tests aren't necessary anymore either. * Fix tests for acceptance matcher * Fix tests for absence matcher
2015-09-26 05:10:00 +00:00
case column_type
when :integer, :float then 1
when :decimal then BigDecimal(1, 0)
when :datetime, :time, :timestamp then Time.current
Tighten CouldNotSetAttributeError restriction Why: * Previously, `allow_value` would raise a CouldNotSetAttributeError if the value being set didn't match the value the attribute had after being set, but only if the attribute was being changed from nil to non-nil or non-nil to nil. * It turns out it doesn't matter which value you're trying to set the attribute to -- if the attribute rejects that change it's confusing either way. (In fact, I was recently bit by a case in which I was trying to validate numericality of an attribute, where the writer method for that attribute was overridden to ensure that the attribute always stored a number and never contained non-number characters. This ended up making the numericality validation useless, of course -- but it caused confusion because the test acted in a way I didn't expect.) To satisfy the above: * `allow_value` now raises a CouldNotSetAttributeError if the attribute rejects the value being set in *any* way. * However, add a `ignoring_interference_by_writer` qualifier so that it is possible to manually override this behavior. * Fix tests that are failing now because of this new change: * Fix tests for allow_value matcher * Fix tests for numericality matcher * Remove tests for numericality matcher + integer column * An integer column will typecast any non-integer value to an integer. * Because of the typecasting, our tests for the numericality matcher against an integer column don't quite work, because we can't really test what happens when the attribute is set to a non-integer value. Now that `allow_value` is more strict, we're getting a CouldNotSetAttributeError when attempting to do so. * The tests mentioned were originally added to ensure that we are handling RangeErrors that ActiveRecord used to emit. This doesn't happen anymore, so the tests aren't necessary anymore either. * Fix tests for acceptance matcher * Fix tests for absence matcher
2015-09-26 05:10:00 +00:00
when :date then Date.new
when :binary then '0'
Tighten CouldNotSetAttributeError restriction Why: * Previously, `allow_value` would raise a CouldNotSetAttributeError if the value being set didn't match the value the attribute had after being set, but only if the attribute was being changed from nil to non-nil or non-nil to nil. * It turns out it doesn't matter which value you're trying to set the attribute to -- if the attribute rejects that change it's confusing either way. (In fact, I was recently bit by a case in which I was trying to validate numericality of an attribute, where the writer method for that attribute was overridden to ensure that the attribute always stored a number and never contained non-number characters. This ended up making the numericality validation useless, of course -- but it caused confusion because the test acted in a way I didn't expect.) To satisfy the above: * `allow_value` now raises a CouldNotSetAttributeError if the attribute rejects the value being set in *any* way. * However, add a `ignoring_interference_by_writer` qualifier so that it is possible to manually override this behavior. * Fix tests that are failing now because of this new change: * Fix tests for allow_value matcher * Fix tests for numericality matcher * Remove tests for numericality matcher + integer column * An integer column will typecast any non-integer value to an integer. * Because of the typecasting, our tests for the numericality matcher against an integer column don't quite work, because we can't really test what happens when the attribute is set to a non-integer value. Now that `allow_value` is more strict, we're getting a CouldNotSetAttributeError when attempting to do so. * The tests mentioned were originally added to ensure that we are handling RangeErrors that ActiveRecord used to emit. This doesn't happen anymore, so the tests aren't necessary anymore either. * Fix tests for acceptance matcher * Fix tests for absence matcher
2015-09-26 05:10:00 +00:00
else 'an arbitrary value'
end
2013-12-05 16:38:30 +00:00
end
end
Tighten CouldNotSetAttributeError restriction Why: * Previously, `allow_value` would raise a CouldNotSetAttributeError if the value being set didn't match the value the attribute had after being set, but only if the attribute was being changed from nil to non-nil or non-nil to nil. * It turns out it doesn't matter which value you're trying to set the attribute to -- if the attribute rejects that change it's confusing either way. (In fact, I was recently bit by a case in which I was trying to validate numericality of an attribute, where the writer method for that attribute was overridden to ensure that the attribute always stored a number and never contained non-number characters. This ended up making the numericality validation useless, of course -- but it caused confusion because the test acted in a way I didn't expect.) To satisfy the above: * `allow_value` now raises a CouldNotSetAttributeError if the attribute rejects the value being set in *any* way. * However, add a `ignoring_interference_by_writer` qualifier so that it is possible to manually override this behavior. * Fix tests that are failing now because of this new change: * Fix tests for allow_value matcher * Fix tests for numericality matcher * Remove tests for numericality matcher + integer column * An integer column will typecast any non-integer value to an integer. * Because of the typecasting, our tests for the numericality matcher against an integer column don't quite work, because we can't really test what happens when the attribute is set to a non-integer value. Now that `allow_value` is more strict, we're getting a CouldNotSetAttributeError when attempting to do so. * The tests mentioned were originally added to ensure that we are handling RangeErrors that ActiveRecord used to emit. This doesn't happen anymore, so the tests aren't necessary anymore either. * Fix tests for acceptance matcher * Fix tests for absence matcher
2015-09-26 05:10:00 +00:00
def column_type
2013-12-05 16:38:30 +00:00
@subject.class.respond_to?(:columns_hash) &&
Tighten CouldNotSetAttributeError restriction Why: * Previously, `allow_value` would raise a CouldNotSetAttributeError if the value being set didn't match the value the attribute had after being set, but only if the attribute was being changed from nil to non-nil or non-nil to nil. * It turns out it doesn't matter which value you're trying to set the attribute to -- if the attribute rejects that change it's confusing either way. (In fact, I was recently bit by a case in which I was trying to validate numericality of an attribute, where the writer method for that attribute was overridden to ensure that the attribute always stored a number and never contained non-number characters. This ended up making the numericality validation useless, of course -- but it caused confusion because the test acted in a way I didn't expect.) To satisfy the above: * `allow_value` now raises a CouldNotSetAttributeError if the attribute rejects the value being set in *any* way. * However, add a `ignoring_interference_by_writer` qualifier so that it is possible to manually override this behavior. * Fix tests that are failing now because of this new change: * Fix tests for allow_value matcher * Fix tests for numericality matcher * Remove tests for numericality matcher + integer column * An integer column will typecast any non-integer value to an integer. * Because of the typecasting, our tests for the numericality matcher against an integer column don't quite work, because we can't really test what happens when the attribute is set to a non-integer value. Now that `allow_value` is more strict, we're getting a CouldNotSetAttributeError when attempting to do so. * The tests mentioned were originally added to ensure that we are handling RangeErrors that ActiveRecord used to emit. This doesn't happen anymore, so the tests aren't necessary anymore either. * Fix tests for acceptance matcher * Fix tests for absence matcher
2015-09-26 05:10:00 +00:00
@subject.class.columns_hash[@attribute.to_s].respond_to?(:type) &&
@subject.class.columns_hash[@attribute.to_s].type
2013-12-05 16:38:30 +00:00
end
def collection?
if reflection
[:has_many, :has_and_belongs_to_many].include?(reflection.macro)
else
false
end
end
def reflection
@subject.class.respond_to?(:reflect_on_association) &&
@subject.class.reflect_on_association(@attribute)
end
def array_column?
@subject.class.respond_to?(:columns_hash) &&
@subject.class.columns_hash[@attribute.to_s].respond_to?(:array) &&
@subject.class.columns_hash[@attribute.to_s].array
end
def enum_column?
@subject.class.respond_to?(:defined_enums) &&
@subject.class.defined_enums.key?(@attribute.to_s)
end
def enum_values
@subject.class.defined_enums[@attribute.to_s].values
end
2013-12-05 16:38:30 +00:00
end
end
end
end