Fix numericality matcher w/ numeric columns

Fix the matcher so it still raises a CouldNotSetAttributeError against a
numeric column, but only if the matcher has not been qualified at all.
When the matcher is qualified with anything else, then it's okay to use
a numeric column, as long as the matcher no longer asserts that the
record disallows a non-numeric value.
This commit is contained in:
Elliot Winkler 2015-10-06 23:12:30 -06:00
parent ba953f8698
commit 18b2859d25
12 changed files with 383 additions and 59 deletions

13
NEWS.md
View File

@ -4,17 +4,24 @@
* Fix `validate_inclusion_of` + `in_array` when used against a date or datetime
column/attribute so that it does not raise a CouldNotSetAttributeError.
([8fa97b4])
([#783], [8fa97b4])
* Fix `validate_numericality_of` when used against a numeric column so that it
no longer raises a CouldNotSetAttributeError if the matcher has been qualified
in any way (`only_integer`, `greater_than`, `odd`, etc.). ([#784])
### Improvements
* `validate_uniqueness_of` now raises a NonCaseSwappableValueError if the value
the matcher is using to test uniqueness cannot be case-swapped -- in other
words, if it doesn't contain any alpha characters. When this is the case, the
matcher cannot work effectively. ([#789])
matcher cannot work effectively. ([#789], [ada9bd3])
[#789]: https://github.com/thoughtbot/shoulda-matchers/pull/789
[#783]: https://github.com/thoughtbot/shoulda-matchers/pull/783
[8fa97b4]: https://github.com/thoughtbot/shoulda-matchers/commit/8fa97b4ff33b57ce16dfb96be1ec892502f2aa9e
[#784]: https://github.com/thoughtbot/shoulda-matchers/pull/784
[#789]: https://github.com/thoughtbot/shoulda-matchers/pull/789
[ada9bd3]: https://github.com/thoughtbot/shoulda-matchers/commit/ada9bd3a1b9f2bb9fa74d0dfe1f8f7080314298c
# 3.0.0

View File

@ -20,7 +20,6 @@ module Shoulda
@value = value
@operator = operator
@message = ERROR_MESSAGES[operator]
@comparison_combos = comparison_combos
@strict = false
end
@ -80,11 +79,7 @@ module Shoulda
def submatchers
@_submatchers ||=
comparison_combos.map do |diff, submatcher_method_name|
matcher = __send__(
submatcher_method_name,
(@value + diff).to_s,
nil
)
matcher = __send__(submatcher_method_name, diff, nil)
matcher.with_message(@message, values: { count: @value })
matcher
end
@ -127,8 +122,14 @@ module Shoulda
end
def diffs_to_compare
diff = @numericality_matcher.diff_to_compare
[-diff, 0, diff]
diff_to_compare = @numericality_matcher.diff_to_compare
values = [-1, 0, 1].map { |sign| @value + (diff_to_compare * sign) }
if @numericality_matcher.given_numeric_column?
values
else
values.map(&:to_s)
end
end
def comparison_expectation

View File

@ -6,14 +6,6 @@ module Shoulda
class EvenNumberMatcher < NumericTypeMatcher
NON_EVEN_NUMBER_VALUE = 1
def initialize(attribute, options = {})
@attribute = attribute
@disallow_value_matcher =
DisallowValueMatcher.new(NON_EVEN_NUMBER_VALUE.to_s).
for(@attribute).
with_message(:even)
end
def allowed_type
'even numbers'
end
@ -21,6 +13,20 @@ module Shoulda
def diff_to_compare
2
end
protected
def wrap_disallow_value_matcher(matcher)
matcher.with_message(:even)
end
def disallowed_value
if @numeric_type_matcher.given_numeric_column?
NON_EVEN_NUMBER_VALUE
else
NON_EVEN_NUMBER_VALUE.to_s
end
end
end
end
end

View File

@ -1,29 +1,37 @@
require 'forwardable'
module Shoulda
module Matchers
module ActiveModel
module NumericalityMatchers
# @private
class NumericTypeMatcher
def initialize
raise NotImplementedError
end
extend Forwardable
def matches?(subject)
@disallow_value_matcher.matches?(subject)
def_delegators :disallow_value_matcher, :matches?, :failure_message,
:failure_message_when_negated
def initialize(numeric_type_matcher, attribute, options = {})
@numeric_type_matcher = numeric_type_matcher
@attribute = attribute
@options = options
@message = nil
@context = nil
@strict = false
end
def with_message(message)
@disallow_value_matcher.with_message(message)
@message = message
self
end
def strict
@disallow_value_matcher.strict
@strict = true
self
end
def on(context)
@disallow_value_matcher.on(context)
@context = context
self
end
@ -35,12 +43,39 @@ module Shoulda
raise NotImplementedError
end
def failure_message
@disallow_value_matcher.failure_message
protected
attr_reader :attribute
def wrap_disallow_value_matcher(matcher)
raise NotImplementedError
end
def failure_message_when_negated
@disallow_value_matcher.failure_message_when_negated
def disallowed_value
raise NotImplementedError
end
private
def disallow_value_matcher
@_disallow_value_matcher ||= begin
DisallowValueMatcher.new(disallowed_value).tap do |matcher|
matcher.for(attribute)
wrap_disallow_value_matcher(matcher)
if @message
matcher.with_message(@message)
end
if @strict
matcher.strict
end
if @context
matcher.on(@context)
end
end
end
end
end
end

View File

@ -4,15 +4,7 @@ module Shoulda
module NumericalityMatchers
# @private
class OddNumberMatcher < NumericTypeMatcher
NON_ODD_NUMBER_VALUE = 2
def initialize(attribute, options = {})
@attribute = attribute
@disallow_value_matcher =
DisallowValueMatcher.new(NON_ODD_NUMBER_VALUE.to_s).
for(@attribute).
with_message(:odd)
end
NON_ODD_NUMBER_VALUE = 2
def allowed_type
'odd numbers'
@ -21,6 +13,20 @@ module Shoulda
def diff_to_compare
2
end
protected
def wrap_disallow_value_matcher(matcher)
matcher.with_message(:odd)
end
def disallowed_value
if @numeric_type_matcher.given_numeric_column?
NON_ODD_NUMBER_VALUE
else
NON_ODD_NUMBER_VALUE.to_s
end
end
end
end
end

View File

@ -5,13 +5,6 @@ module Shoulda
# @private
class OnlyIntegerMatcher < NumericTypeMatcher
NON_INTEGER_VALUE = 0.1
def initialize(attribute)
@attribute = attribute
@disallow_value_matcher =
DisallowValueMatcher.new(NON_INTEGER_VALUE.to_s).
for(attribute).
with_message(:not_an_integer)
end
def allowed_type
'integers'
@ -20,6 +13,20 @@ module Shoulda
def diff_to_compare
1
end
protected
def wrap_disallow_value_matcher(matcher)
matcher.with_message(:not_an_integer)
end
def disallowed_value
if @numeric_type_matcher.given_numeric_column?
NON_INTEGER_VALUE
else
NON_INTEGER_VALUE.to_s
end
end
end
end
end

View File

@ -315,18 +315,19 @@ module Shoulda
@submatchers = []
@diff_to_compare = DEFAULT_DIFF_TO_COMPARE
@strict = false
add_disallow_value_matcher
end
def strict
@submatchers.each(&:strict)
@strict = true
@submatchers.each(&:strict)
self
end
def only_integer
prepare_submatcher(
NumericalityMatchers::OnlyIntegerMatcher.new(@attribute)
NumericalityMatchers::OnlyIntegerMatcher.new(self, @attribute)
)
self
end
@ -342,14 +343,14 @@ module Shoulda
def odd
prepare_submatcher(
NumericalityMatchers::OddNumberMatcher.new(@attribute)
NumericalityMatchers::OddNumberMatcher.new(self, @attribute)
)
self
end
def even
prepare_submatcher(
NumericalityMatchers::EvenNumberMatcher.new(@attribute)
NumericalityMatchers::EvenNumberMatcher.new(self, @attribute)
)
self
end
@ -391,6 +392,19 @@ module Shoulda
def matches?(subject)
@subject = subject
if given_numeric_column?
remove_disallow_value_matcher
end
if @submatchers.empty?
raise IneffectiveTestError.create(
model: @subject.class,
attribute: @attribute,
column_type: column_type
)
end
first_failing_submatcher.nil?
end
@ -417,8 +431,18 @@ module Shoulda
first_failing_submatcher.failure_message_when_negated
end
def given_numeric_column?
[:integer, :float].include?(column_type)
end
private
def column_type
if @subject.class.respond_to?(:columns_hash)
@subject.class.columns_hash[@attribute.to_s].type
end
end
def add_disallow_value_matcher
disallow_value_matcher = DisallowValueMatcher.new(NON_NUMERIC_VALUE).
for(@attribute).
@ -427,8 +451,13 @@ module Shoulda
add_submatcher(disallow_value_matcher)
end
def remove_disallow_value_matcher
@submatchers.shift
end
def prepare_submatcher(submatcher)
add_submatcher(submatcher)
if submatcher.respond_to?(:diff_to_compare)
update_diff_to_compare(submatcher)
end
@ -476,6 +505,32 @@ module Shoulda
arr
end
end
class IneffectiveTestError < Shoulda::Matchers::Error
attr_accessor :model, :attribute, :column_type
def message
Shoulda::Matchers.word_wrap \
<<MESSAGE
You are attempting to use validate_numericality_of, but the attribute you're
testing, :#{attribute}, is a #{column_type} column. One of the things that the
numericality matcher does is to assert that setting :#{attribute} to a string
that doesn't look like a #{column_type} will cause your
#{humanized_model_name} to become invalid. In this case, it's impossible to make
this assertion since :#{attribute} will typecast any incoming value to a
#{column_type}. This means that it's already guaranteed to be numeric!
Since this matcher isn't doing anything, you can remove it from your model
tests, and in fact, you can remove the validation from your model as it isn't
doing anything either.
MESSAGE
end
private
def humanized_model_name
model.name.humanize.downcase
end
end
end
end
end

View File

@ -283,6 +283,6 @@ describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::ComparisonMatcher
end
def numericality_matcher
double(diff_to_compare: 1)
double(diff_to_compare: 1, given_numeric_column?: nil)
end
end

View File

@ -1,7 +1,7 @@
require 'unit_spec_helper'
describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::EvenNumberMatcher do
subject { described_class.new(:attr) }
subject { described_class.new(numericality_matcher, :attr) }
it_behaves_like 'a numerical submatcher'
it_behaves_like 'a numerical type submatcher'
@ -84,6 +84,10 @@ describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::EvenNumberMatcher
end
end
def numericality_matcher
double(:numericality_matcher, given_numeric_column?: nil)
end
def validating_even_number(options = {})
define_model :example, attr: :string do
validates_numericality_of :attr, { even: true }.merge(options)
@ -93,5 +97,4 @@ describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::EvenNumberMatcher
def not_validating_even_number
define_model(:example, attr: :string).new
end
end

View File

@ -1,7 +1,7 @@
require 'unit_spec_helper'
describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::OddNumberMatcher do
subject { described_class.new(:attr) }
subject { described_class.new(numericality_matcher, :attr) }
it_behaves_like 'a numerical submatcher'
it_behaves_like 'a numerical type submatcher'
@ -84,6 +84,10 @@ describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::OddNumberMatcher
end
end
def numericality_matcher
double(:numericality_matcher, given_numeric_column?: nil)
end
def validating_odd_number(options = {})
define_model :example, attr: :string do
validates_numericality_of :attr, { odd: true }.merge(options)
@ -93,5 +97,4 @@ describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::OddNumberMatcher
def not_validating_odd_number
define_model(:example, attr: :string).new
end
end

View File

@ -1,7 +1,7 @@
require 'unit_spec_helper'
describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::OnlyIntegerMatcher do
subject { described_class.new(:attr) }
subject { described_class.new(numericality_matcher, :attr) }
it_behaves_like 'a numerical submatcher'
it_behaves_like 'a numerical type submatcher'
@ -84,6 +84,10 @@ describe Shoulda::Matchers::ActiveModel::NumericalityMatchers::OnlyIntegerMatche
end
end
def numericality_matcher
double(:numericality_matcher, given_numeric_column?: nil)
end
def validating_only_integer(options = {})
define_model :example, attr: :string do
validates_numericality_of :attr, { only_integer: true }.merge(options)

View File

@ -126,6 +126,30 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher, type: :m
record = build_record_validating_numericality
expect(record).to validate_numericality
end
context 'when the column is an integer column' do
it 'raises an IneffectiveTestError' do
record = build_record_validating_numericality(
column_type: :integer
)
assertion = -> { expect(record).to validate_numericality }
expect(&assertion).
to raise_error(described_class::IneffectiveTestError)
end
end
context 'when the column is a float column' do
it 'raises an IneffectiveTestError' do
record = build_record_validating_numericality(
column_type: :float
)
assertion = -> { expect(record).to validate_numericality }
expect(&assertion).
to raise_error(described_class::IneffectiveTestError)
end
end
end
context 'and not validating anything' do
@ -187,6 +211,28 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher, type: :m
record = build_record_validating_numericality(odd: true)
expect(record).to validate_numericality.odd
end
context 'when the column is an integer column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :integer,
odd: true
)
expect(record).to validate_numericality.odd
end
end
context 'when the column is a float column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :float,
odd: true
)
expect(record).to validate_numericality.odd
end
end
end
context 'and not validating with odd' do
@ -204,10 +250,32 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher, type: :m
context 'qualified with even' do
context 'and validating with even' do
it 'allows even number values for that attribute' do
it 'accepts' do
record = build_record_validating_numericality(even: true)
expect(record).to validate_numericality.even
end
context 'when the column is an integer column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :integer,
even: true
)
expect(record).to validate_numericality.even
end
end
context 'when the column is a float column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :float,
even: true
)
expect(record).to validate_numericality.even
end
end
end
context 'and not validating with even' do
@ -229,6 +297,28 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher, type: :m
)
expect(record).to validate_numericality.is_less_than_or_equal_to(18)
end
context 'when the column is an integer column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :integer,
less_than_or_equal_to: 18
)
expect(record).to validate_numericality.is_less_than_or_equal_to(18)
end
end
context 'when the column is a float column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :float,
less_than_or_equal_to: 18
)
expect(record).to validate_numericality.is_less_than_or_equal_to(18)
end
end
end
context 'and not validating with less_than_or_equal_to' do
@ -254,6 +344,28 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher, type: :m
to validate_numericality.
is_less_than(18)
end
context 'when the column is an integer column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :integer,
less_than: 18
)
expect(record).to validate_numericality.is_less_than(18)
end
end
context 'when the column is a float column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :float,
less_than: 18
)
expect(record).to validate_numericality.is_less_than(18)
end
end
end
context 'and not validating with less_than' do
@ -277,6 +389,28 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher, type: :m
record = build_record_validating_numericality(equal_to: 18)
expect(record).to validate_numericality.is_equal_to(18)
end
context 'when the column is an integer column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :integer,
equal_to: 18
)
expect(record).to validate_numericality.is_equal_to(18)
end
end
context 'when the column is a float column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :float,
equal_to: 18
)
expect(record).to validate_numericality.is_equal_to(18)
end
end
end
context 'and not validating with equal_to' do
@ -302,6 +436,32 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher, type: :m
to validate_numericality.
is_greater_than_or_equal_to(18)
end
context 'when the column is an integer column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :integer,
greater_than_or_equal_to: 18
)
expect(record).
to validate_numericality.
is_greater_than_or_equal_to(18)
end
end
context 'when the column is a float column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :float,
greater_than_or_equal_to: 18
)
expect(record).
to validate_numericality.
is_greater_than_or_equal_to(18)
end
end
end
context 'not validating with greater_than_or_equal_to' do
@ -327,6 +487,32 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher, type: :m
to validate_numericality.
is_greater_than(18)
end
context 'when the column is an integer column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :integer,
greater_than: 18
)
expect(record).
to validate_numericality.
is_greater_than(18)
end
end
context 'when the column is a float column' do
it 'accepts (and does not raise an error)' do
record = build_record_validating_numericality(
column_type: :float,
greater_than: 18
)
expect(record).
to validate_numericality.
is_greater_than(18)
end
end
end
context 'and not validating with greater_than' do
@ -740,6 +926,16 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher, type: :m
end
end
context 'against an ActiveModel model' do
it 'accepts' do
model = define_active_model_class :example, accessors: [:attr] do
validates_numericality_of :attr
end
expect(model.new).to validate_numericality_of(:attr)
end
end
describe '#description' do
context 'qualified with nothing' do
it 'describes that it allows numbers' do
@ -845,8 +1041,9 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher, type: :m
def define_model_validating_numericality(options = {})
attribute_name = options.delete(:attribute_name) { self.attribute_name }
column_type = options.delete(:column_type) { :string }
define_model 'Example', attribute_name => :string do |model|
define_model 'Example', attribute_name => { type: column_type } do |model|
model.validates_numericality_of(attribute_name, options)
end
end