1
0
Fork 0
mirror of https://github.com/thoughtbot/shoulda-matchers.git synced 2022-11-09 12:01:38 -05:00

Refactor allow_value & disallow_value

This is part of a collection of commits that aim to improve failure
messages across the board, in order to make matchers easier to debug
when something goes wrong.

* Have the failure message describe more clearly what the `allow_value`
  matcher was trying to do when it failed.
* Make the description of the matcher more readable.
* For each value that `allow_value` sets, use a different Validator
  instance. The matcher still changes state as it runs, but a future
  commit will refactor this further.
* Merge StrictValidator back into Validator, and remove it. The way that
  StrictValidator worked (as a module that was mixed into an instance of
  Validator at runtime) was confusing, and there's really no need to
  split out the logic anymore.
* Fix or fill in tests involving failure messages and descriptions.
This commit is contained in:
Elliot Winkler 2015-12-13 16:55:34 -07:00
parent 8361f39579
commit 583be384c3
7 changed files with 517 additions and 290 deletions

View file

@ -2,7 +2,6 @@ require 'shoulda/matchers/active_model/helpers'
require 'shoulda/matchers/active_model/validation_matcher' require 'shoulda/matchers/active_model/validation_matcher'
require 'shoulda/matchers/active_model/validation_matcher/build_description' require 'shoulda/matchers/active_model/validation_matcher/build_description'
require 'shoulda/matchers/active_model/validator' require 'shoulda/matchers/active_model/validator'
require 'shoulda/matchers/active_model/strict_validator'
require 'shoulda/matchers/active_model/allow_value_matcher' require 'shoulda/matchers/active_model/allow_value_matcher'
require 'shoulda/matchers/active_model/disallow_value_matcher' require 'shoulda/matchers/active_model/disallow_value_matcher'
require 'shoulda/matchers/active_model/validate_length_of_matcher' require 'shoulda/matchers/active_model/validate_length_of_matcher'

View file

@ -354,99 +354,194 @@ https://github.com/thoughtbot/shoulda-matchers/issues
include Helpers include Helpers
attr_accessor :attribute_with_message attr_accessor :failure_message_preface
attr_accessor :options attr_reader :last_value_set
def initialize(*values) def initialize(*values)
self.values_to_match = values @values_to_set = values
self.options = {} @options = {}
self.after_setting_value_callback = -> {} @after_setting_value_callback = -> {}
self.validator = Validator.new
@ignoring_interference_by_writer = false @ignoring_interference_by_writer = false
@expects_strict = false
@expects_custom_validation_message = false
@context = nil
@failure_message_preface = proc do
<<-PREFIX.strip_heredoc.strip
After setting :#{attribute_to_set} to #{last_value_set.inspect},
the matcher expected the #{model.name} to be
PREFIX
end
end end
def for(attribute) def for(attribute)
self.attribute_to_set = attribute @attribute_to_set = attribute
self.attribute_to_check_message_against = attribute @attribute_to_check_message_against = attribute
self self
end end
def on(context) def on(context)
validator.context = context if context.present?
self @context = context
end
def with_message(message, options={})
self.options[:expected_message] = message
self.options[:expected_message_values] = options.fetch(:values, {})
if options.key?(:against)
self.attribute_to_check_message_against = options[:against]
end end
self self
end end
def strict def with_message(message, given_options = {})
validator.strict = true if message.present?
@expects_custom_validation_message = true
options[:expected_message] = message
options[:expected_message_values] = given_options.fetch(:values, {})
if given_options.key?(:against)
@attribute_to_check_message_against = given_options[:against]
end
end
self self
end end
def expects_custom_validation_message?
@expects_custom_validation_message
end
def strict(expects_strict = true)
@expects_strict = expects_strict
self
end
def expects_strict?
@expects_strict
end
def ignoring_interference_by_writer def ignoring_interference_by_writer
@ignoring_interference_by_writer = true @ignoring_interference_by_writer = true
self self
end end
def _after_setting_value(&callback) def _after_setting_value(&callback)
self.after_setting_value_callback = callback @after_setting_value_callback = callback
end end
def matches?(instance) def matches?(instance)
self.instance = instance @instance = instance
values_to_match.all? { |value| value_matches?(value) } first_failing_value_and_validator.nil?
end end
def does_not_match?(instance) def does_not_match?(instance)
self.instance = instance @instance = instance
values_to_match.all? { |value| !value_matches?(value) } first_passing_value_and_validator.nil?
end end
def failure_message def failure_message
"Did not expect #{expectation},\ngot#{error_description}" validator = first_failing_validator
message =
failure_message_preface.call +
' valid, but it was invalid instead,'
if validator.captured_validation_exception?
message << ' raising a validation exception with the message '
message << validator.validation_exception_message.inspect
message << '.'
else
message << " producing these validation errors:\n\n"
message << validator.all_formatted_validation_error_messages
end
Shoulda::Matchers.word_wrap(message)
end end
def failure_message_when_negated def failure_message_when_negated
"Expected #{expectation},\ngot#{error_description}" validator = first_passing_validator
message = failure_message_preface.call + ' invalid'
if validator.type_of_message_matched?
if validator.has_messages?
message << ' and to'
if validator.captured_validation_exception?
message << ' raise a validation exception with message'
else
message << ' produce'
if expected_message.is_a?(Regexp)
message << ' a'
else
message << ' the'
end
message << ' validation error'
end
if expected_message.is_a?(Regexp)
message << ' matching'
end
message << " #{expected_message.inspect}"
unless validator.captured_validation_exception?
message << " on :#{attribute_to_check_message_against}"
end
message << '. The record was indeed invalid, but'
if validator.captured_validation_exception?
message << ' the exception message was '
message << validator.validation_exception_message.inspect
message << ' instead.'
else
message << " it produced these validation errors instead:\n\n"
message << validator.all_formatted_validation_error_messages
end
else
message << ', but it was valid instead.'
end
elsif validator.captured_validation_exception?
message << ' and to produce validation errors, but the record'
message << ' raised a validation exception instead.'
else
message << ' and to raise a validation exception, but the record'
message << ' produced validation errors instead.'
end
Shoulda::Matchers.word_wrap(message)
end
def simple_description
"allow :#{attribute_to_set} to be #{inspected_values_to_set}"
end end
def description def description
validator.allow_description(allowed_values) ValidationMatcher::BuildDescription.call(self, simple_description)
end
def model
instance.class
end end
protected protected
attr_reader :instance, :attribute_to_check_message_against attr_reader(
attr_accessor :values_to_match, :attribute_to_set, :value, :after_setting_value_callback,
:matched_error, :after_setting_value_callback, :validator :attribute_to_check_message_against,
:attribute_to_set,
:context,
:instance,
:options,
:values_to_set,
)
def instance=(instance) private
@instance = instance
validator.record = instance
end
def attribute_to_check_message_against=(attribute)
@attribute_to_check_message_against = attribute
validator.attribute = attribute
end
def ignoring_interference_by_writer? def ignoring_interference_by_writer?
@ignoring_interference_by_writer @ignoring_interference_by_writer
end end
def value_matches?(value) def value_matches?(value, validator)
self.value = value @last_value_set = value
set_attribute(value) set_attribute(value)
!(errors_match? || any_range_error_occurred?) !errors_match?(validator) && !any_range_error_occurred?(validator)
end end
def set_attribute(value) def set_attribute(value)
@ -468,64 +563,36 @@ https://github.com/thoughtbot/shoulda-matchers/issues
end end
end end
def errors_match? def errors_match?(validator)
has_messages? && errors_for_attribute_match? validator.has_messages? &&
validator.type_of_message_matched? &&
errors_for_attribute_match?(validator)
end end
def has_messages? def errors_for_attribute_match?(validator)
validator.has_messages? matched_errors(validator).compact.any?
end end
def errors_for_attribute_match? def matched_errors(validator)
if expected_message if expected_message
self.matched_error = errors_match_regexp? || errors_match_string? validator.messages.grep(expected_message)
else else
errors_for_attribute.compact.any? validator.messages
end end
end end
def errors_for_attribute def any_range_error_occurred?(validator)
validator.formatted_messages
end
def errors_match_regexp?
if Regexp === expected_message
errors_for_attribute.detect { |e| e =~ expected_message }
end
end
def errors_match_string?
if errors_for_attribute.include?(expected_message)
expected_message
end
end
def any_range_error_occurred?
validator.captured_range_error? validator.captured_range_error?
end end
def expectation def inspected_values_to_set
parts = [ if values_to_set.size > 1
expected_messages_description, values_to_set.map(&:inspect).to_sentence(
"when #{attribute_to_set} is set to #{value.inspect}" two_words_connector: " or ",
] last_word_connector: ", or"
)
parts.join(' ').squeeze(' ')
end
def expected_messages_description
validator.expected_messages_description(expected_message)
end
def error_description
validator.messages_description
end
def allowed_values
if values_to_match.length > 1
"any of [#{values_to_match.map(&:inspect).join(', ')}]"
else else
values_to_match.first.inspect values_to_set.first.inspect
end end
end end
@ -540,7 +607,11 @@ https://github.com/thoughtbot/shoulda-matchers/issues
end end
def default_expected_message def default_expected_message
validator.expected_message_from(default_attribute_message) if expects_strict?
"#{human_attribute_name} #{default_attribute_message}"
else
default_attribute_message
end
end end
def default_attribute_message def default_attribute_message
@ -560,9 +631,51 @@ https://github.com/thoughtbot/shoulda-matchers/issues
defaults.merge(options[:expected_message_values]) defaults.merge(options[:expected_message_values])
end end
def values_and_validators
@_values_and_validators ||= values_to_set.map do |value|
[value, build_validator]
end
end
def build_validator
validator = Validator.new(
instance,
attribute_to_check_message_against
)
validator.context = context
validator.expects_strict = expects_strict?
validator
end
def first_failing_value_and_validator
@_first_failing_value_and_validator ||=
values_and_validators.detect do |value, validator|
!value_matches?(value, validator)
end
end
def first_failing_validator
first_failing_value_and_validator[1]
end
def first_passing_value_and_validator
@_first_passing_value_and_validator ||=
values_and_validators.detect do |value, validator|
value_matches?(value, validator)
end
end
def first_passing_validator
first_passing_value_and_validator[1]
end
def model_name def model_name
instance.class.to_s.underscore instance.class.to_s.underscore
end end
def human_attribute_name
instance.class.human_attribute_name(attribute_to_check_message_against)
end
end end
end end
end end

View file

@ -7,46 +7,55 @@ module Shoulda
class DisallowValueMatcher class DisallowValueMatcher
extend Forwardable extend Forwardable
def_delegators :allow_matcher, :_after_setting_value def_delegators :allow_matcher,
:_after_setting_value,
:attribute_to_set,
:description,
:model,
:last_value_set,
:simple_description,
:failure_message_preface,
:failure_message_preface=
def initialize(value) def initialize(value)
@allow_matcher = AllowValueMatcher.new(value) @allow_matcher = AllowValueMatcher.new(value)
end end
def matches?(subject) def matches?(subject)
!@allow_matcher.matches?(subject) !allow_matcher.matches?(subject)
end end
def for(attribute) def for(attribute)
@allow_matcher.for(attribute) allow_matcher.for(attribute)
self self
end end
def on(context) def on(context)
@allow_matcher.on(context) allow_matcher.on(context)
self self
end end
def with_message(message, options={}) def with_message(message, options={})
@allow_matcher.with_message(message, options) allow_matcher.with_message(message, options)
self
end
def strict(strict = true)
allow_matcher.strict(strict)
self self
end end
def ignoring_interference_by_writer def ignoring_interference_by_writer
@allow_matcher.ignoring_interference_by_writer allow_matcher.ignoring_interference_by_writer
self self
end end
def failure_message def failure_message
@allow_matcher.failure_message_when_negated allow_matcher.failure_message_when_negated
end end
def failure_message_when_negated def failure_message_when_negated
@allow_matcher.failure_message allow_matcher.failure_message
end
def strict
@allow_matcher.strict
self
end end
protected protected

View file

@ -3,25 +3,17 @@ module Shoulda
module ActiveModel module ActiveModel
# @private # @private
module Helpers module Helpers
def pretty_error_messages(obj) def pretty_error_messages(object)
obj.errors.map do |attribute, message| format_validation_errors(object.errors)
full_message = message.dup.inspect
parenthetical_parts = []
unless attribute.to_sym == :base
parenthetical_parts << "attribute: #{attribute}"
if obj.respond_to?(attribute)
parenthetical_parts << "value: #{obj.__send__(attribute).inspect}"
end
end end
if parenthetical_parts.any? def format_validation_errors(errors)
full_message << " (#{parenthetical_parts.join(', ')})" list_items = errors.keys.map do |attribute|
messages = errors[attribute]
"* #{attribute}: #{messages}"
end end
"* " + full_message list_items.join("\n")
end.join("\n")
end end
def default_error_message(type, options = {}) def default_error_message(type, options = {})

View file

@ -1,51 +0,0 @@
module Shoulda
module Matchers
module ActiveModel
# @private
module StrictValidator
def allow_description(allowed_values)
"doesn't raise when #{attribute} is set to #{allowed_values}"
end
def expected_message_from(attribute_message)
"#{human_attribute_name} #{attribute_message}"
end
def formatted_messages
[messages.first.message]
end
def messages_description
if has_messages?
': ' + messages.first.message.inspect
else
' no exception'
end
end
def expected_messages_description(expected_message)
if expected_message
"exception to include #{expected_message.inspect}"
else
'an exception to have been raised'
end
end
protected
def collect_messages
validation_exceptions
end
private
def validation_exceptions
record.valid?(context)
[]
rescue ::ActiveModel::StrictValidationFailed => exception
[exception]
end
end
end
end
end

View file

@ -5,99 +5,104 @@ module Shoulda
class Validator class Validator
include Helpers include Helpers
attr_writer :attribute, :context, :record attr_writer :context, :expects_strict
def initialize def initialize(record, attribute)
@record = record
@attribute = attribute
@context = context
@expects_strict = false
reset reset
end end
def reset def reset
@messages = nil @_validation_result = nil
end @captured_validation_exception = false
@captured_range_error = false
def strict=(strict)
@strict = strict
if strict
extend StrictValidator
end
end
def allow_description(allowed_values)
"allow #{attribute} to be set to #{allowed_values}"
end
def expected_message_from(attribute_message)
attribute_message
end end
def messages def messages
@messages ||= collect_messages if expects_strict?
[validation_exception_message]
else
validation_error_messages
end end
def formatted_messages
messages
end end
def has_messages? def has_messages?
messages.any? messages.any?
end end
def messages_description def type_of_message_matched?
if has_messages? expects_strict? == captured_validation_exception?
" errors:\n#{pretty_error_messages(record)}"
else
' no errors'
end
end end
def expected_messages_description(expected_message) def captured_validation_exception?
if expected_message @captured_validation_exception
"errors to include #{expected_message.inspect}"
else
'errors'
end
end end
def captured_range_error? def captured_range_error?
!!captured_range_error !!@captured_range_error
end
def all_validation_errors
validation_result[:all_validation_errors]
end
def all_formatted_validation_error_messages
format_validation_errors(all_validation_errors)
end
def validation_error_messages
validation_result[:validation_error_messages]
end
def validation_exception_message
validation_result[:validation_exception_message]
end end
protected protected
attr_reader :attribute, :context, :strict, :record, attr_reader :attribute, :context, :record
:captured_range_error
def collect_messages
validation_errors
end
private private
def strict? def expects_strict?
!!@strict @expects_strict
end end
def collect_errors_or_exceptions def validation_result
collect_messages @_validation_result ||= perform_validation
end end
def validation_errors def perform_validation
if context if context
record.valid?(context) record.valid?(context)
else else
record.valid? record.valid?
end end
all_validation_errors = record.errors.dup
validation_error_messages =
if record.errors.respond_to?(:[]) if record.errors.respond_to?(:[])
record.errors[attribute] record.errors[attribute]
else else
record.errors.on(attribute) record.errors.on(attribute)
end end
end
def human_attribute_name {
record.class.human_attribute_name(attribute) all_validation_errors: all_validation_errors,
validation_error_messages: validation_error_messages,
validation_exception_message: nil
}
rescue ::ActiveModel::StrictValidationFailed => exception
@captured_validation_exception = true
{
all_validation_errors: nil,
validation_error_messages: [],
validation_exception_message: exception.message
}
end end
end end
end end

View file

@ -13,21 +13,24 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
it 'describes itself with multiple values' do it 'describes itself with multiple values' do
matcher = allow_value('foo', 'bar').for(:baz) matcher = allow_value('foo', 'bar').for(:baz)
expect(matcher.description).to eq 'allow baz to be set to any of ["foo", "bar"]' expect(matcher.description).to eq(
'allow :baz to be "foo" or "bar"'
)
end end
it 'describes itself with a single value' do it 'describes itself with a single value' do
matcher = allow_value('foo').for(:baz) matcher = allow_value('foo').for(:baz)
expect(matcher.description).to eq 'allow baz to be set to "foo"' expect(matcher.description).to eq 'allow :baz to be "foo"'
end end
if active_model_3_2? if active_model_3_2?
it 'describes itself with a strict validation' do it 'describes itself with a strict validation' do
strict_matcher = allow_value('xyz').for(:attr).strict strict_matcher = allow_value('xyz').for(:attr).strict
expect(strict_matcher.description). expect(strict_matcher.description).to eq(
to eq %q(doesn't raise when attr is set to "xyz") 'allow :attr to be "xyz", raising a validation exception on failure'
)
end end
end end
end end
@ -51,8 +54,19 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
expect(validating_format(with: /abc/)).to allow_value('abcde').for(:attr) expect(validating_format(with: /abc/)).to allow_value('abcde').for(:attr)
end end
it 'rejects a bad value' do it 'rejects a bad value with an appropriate failure message' do
expect(validating_format(with: /abc/)).not_to allow_value('xyz').for(:attr) message = <<-MESSAGE
After setting :attr to "xyz", the matcher expected the Example to be
valid, but it was invalid instead, producing these validation errors:
* attr: ["is invalid"]
MESSAGE
assertion = lambda do
expect(validating_format(with: /abc/)).to allow_value('xyz').for(:attr)
end
expect(&assertion).to fail_with_message(message)
end end
it 'allows several good values' do it 'allows several good values' do
@ -60,12 +74,32 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
to allow_value('abcde', 'deabc').for(:attr) to allow_value('abcde', 'deabc').for(:attr)
end end
it 'rejects several bad values' do context 'given several bad values' do
it 'rejects' do
expect(validating_format(with: /abc/)). expect(validating_format(with: /abc/)).
not_to allow_value('xyz', 'zyx', nil, []). not_to allow_value('xyz', 'zyx', nil, []).
for(:attr). for(:attr).
ignoring_interference_by_writer ignoring_interference_by_writer
end end
it 'produces an appropriate failure message' do
message = <<-MESSAGE
After setting :attr to "zyx", the matcher expected the Example to be
valid, but it was invalid instead, producing these validation errors:
* attr: ["is invalid"]
MESSAGE
assertion = lambda do
expect(validating_format(with: /abc/)).
to allow_value('abc', 'abcde', 'zyx', nil, []).
for(:attr).
ignoring_interference_by_writer
end
expect(&assertion).to fail_with_message(message)
end
end
end end
context 'an attribute with a validation and a custom message' do context 'an attribute with a validation and a custom message' do
@ -74,12 +108,45 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
to allow_value('abcde').for(:attr).with_message(/bad/) to allow_value('abcde').for(:attr).with_message(/bad/)
end end
it 'rejects a bad value' do it 'rejects a bad value with an appropriate failure message' do
message = <<-MESSAGE
After setting :attr to "xyz", the matcher expected the Example to be
valid, but it was invalid instead, producing these validation errors:
* attr: ["bad value"]
MESSAGE
assertion = lambda do
expect(validating_format(with: /abc/, message: 'bad value')). expect(validating_format(with: /abc/, message: 'bad value')).
not_to allow_value('xyz').for(:attr).with_message(/bad/) to allow_value('xyz').for(:attr).with_message(/bad/)
end end
it 'allows interpolation values for the message to be provided' do expect(&assertion).to fail_with_message(message)
end
context 'when the custom messages do not match' do
it 'rejects with an appropriate failure message' do
message = <<-MESSAGE
After setting :attr to "xyz", the matcher expected the Example to be
invalid and to produce a validation error matching /different/ on :attr.
The record was indeed invalid, but it produced these validation errors
instead:
* attr: ["bad value"]
MESSAGE
assertion = lambda do
expect(validating_format(with: /abc/, message: 'bad value')).
not_to allow_value('xyz').for(:attr).with_message(/different/)
end
expect(&assertion).to fail_with_message(message)
end
end
context 'when interpolation values are provided along with a custom message' do
context 'when the messages match' do
it 'accepts' do
options = { options = {
attribute_name: :attr, attribute_name: :attr,
attribute_type: :string attribute_type: :string
@ -98,30 +165,102 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
end end
end end
context 'when the messages do not match' do
it 'rejects with an appropriate failure message' do
options = {
attribute_name: :attr,
attribute_type: :string
}
record = record_with_custom_validation(options) do
if self.attr == 'xyz'
self.errors.add :attr, "some other error"
end
end
assertion = lambda do
expect(record).
not_to allow_value('xyz').
for(:attr).
with_message(:greater_than, values: { count: 2 })
end
message = <<-MESSAGE
After setting :attr to "xyz", the matcher expected the Example to be
invalid and to produce the validation error "must be greater than 2" on
:attr. The record was indeed invalid, but it produced these validation
errors instead:
* attr: ["some other error"]
MESSAGE
expect(&assertion).to fail_with_message(message)
end
end
end
end
context 'when the attribute being validated is different than the attribute that receives the validation error' do context 'when the attribute being validated is different than the attribute that receives the validation error' do
include UnitTests::AllowValueMatcherHelpers include UnitTests::AllowValueMatcherHelpers
context 'when the validation error message was provided directly' do context 'when the validation error message was provided directly' do
it 'passes given a valid value' do context 'given a valid value' do
it 'accepts' do
builder = builder_for_record_with_different_error_attribute builder = builder_for_record_with_different_error_attribute
expect(builder.record). expect(builder.record).
to allow_value(builder.valid_value). to allow_value(builder.valid_value).
for(builder.attribute_to_validate). for(builder.attribute_to_validate).
with_message(builder.message, with_message(
builder.message,
against: builder.attribute_that_receives_error
)
end
end
context 'given an invalid value' do
it 'rejects' do
builder = builder_for_record_with_different_error_attribute
invalid_value = "#{builder.valid_value} (invalid)"
expect(builder.record).
not_to allow_value(invalid_value).
for(builder.attribute_to_validate).
with_message(
builder.message,
against: builder.attribute_that_receives_error against: builder.attribute_that_receives_error
) )
end end
it 'fails given an invalid value' do context 'if the messages do not match' do
builder = builder_for_record_with_different_error_attribute it 'technically accepts' do
builder = builder_for_record_with_different_error_attribute(
message: "a different error"
)
invalid_value = "#{builder.valid_value} (invalid)" invalid_value = "#{builder.valid_value} (invalid)"
assertion = lambda do
expect(builder.record). expect(builder.record).
not_to allow_value(invalid_value). not_to allow_value(invalid_value).
for(builder.attribute_to_validate). for(builder.attribute_to_validate).
with_message(builder.message, with_message(
"some error",
against: builder.attribute_that_receives_error against: builder.attribute_that_receives_error
) )
end end
message = <<-MESSAGE
After setting :#{builder.attribute_to_validate} to "#{invalid_value}", the
matcher expected the #{builder.model.name} to be invalid and to produce the validation
error "some error" on :#{builder.attribute_that_receives_error}. The record was
indeed invalid, but it produced these validation errors instead:
* #{builder.attribute_that_receives_error}: ["a different error"]
MESSAGE
expect(&assertion).to fail_with_message(message)
end
end
end
end end
context 'when the validation error message was provided via i18n' do context 'when the validation error message was provided via i18n' do
@ -130,7 +269,8 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
expect(builder.record). expect(builder.record).
to allow_value(builder.valid_value). to allow_value(builder.valid_value).
for(builder.attribute_to_validate). for(builder.attribute_to_validate).
with_message(builder.validation_message_key, with_message(
builder.validation_message_key,
against: builder.attribute_that_receives_error against: builder.attribute_that_receives_error
) )
end end
@ -141,7 +281,8 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
expect(builder.record). expect(builder.record).
not_to allow_value(invalid_value). not_to allow_value(invalid_value).
for(builder.attribute_to_validate). for(builder.attribute_to_validate).
with_message(builder.validation_message_key, with_message(
builder.validation_message_key,
against: builder.attribute_that_receives_error against: builder.attribute_that_receives_error
) )
end end
@ -199,11 +340,16 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
end end
it "does not match given good values along with bad values" do it "does not match given good values along with bad values" do
message = %{Expected errors when attr is set to "12345",\ngot no errors} message = <<-MESSAGE.strip_heredoc
After setting :attr to "12345", the matcher expected the Example to be
invalid, but it was valid instead.
MESSAGE
expect { assertion = lambda do
expect(model).not_to allow_value('12345', *bad_values).for(:attr) expect(model).not_to allow_value('12345', *bad_values).for(:attr)
}.to fail_with_message(message) end
expect(&assertion).to fail_with_message(message)
end end
end end
@ -226,25 +372,39 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
if active_model_3_2? if active_model_3_2?
context 'an attribute with a strict format validation' do context 'an attribute with a strict format validation' do
it 'strictly rejects a bad value' do context 'when qualified with strict' do
it 'rejects a bad value, providing the correct failure message' do
message = <<-MESSAGE.strip_heredoc
After setting :attr to "xyz", the matcher expected the Example to be
valid, but it was invalid instead, raising a validation exception with
the message "Attr is invalid".
MESSAGE
assertion = lambda do
expect(validating_format(with: /abc/, strict: true)). expect(validating_format(with: /abc/, strict: true)).
not_to allow_value('xyz').for(:attr).strict to allow_value('xyz').for(:attr).strict
end end
it 'strictly allows a bad value with a different message' do expect(&assertion).to fail_with_message(message)
expect(validating_format(with: /abc/, strict: true)).
to allow_value('xyz').for(:attr).with_message(/abc/).strict
end end
it 'provides a useful negative failure message' do context 'qualified with a custom message' do
matcher = allow_value('xyz').for(:attr).strict.with_message(/abc/) it 'rejects a bad value when the failure messages do not match' do
message = <<-MESSAGE.strip_heredoc
After setting :attr to "xyz", the matcher expected the Example to be
invalid and to raise a validation exception with message matching /abc/.
The record was indeed invalid, but the exception message was "Attr is
invalid" instead.
MESSAGE
matcher.matches?(validating_format(with: /abc/, strict: true)) assertion = lambda do
expect(validating_format(with: /abc/, strict: true)).
not_to allow_value('xyz').for(:attr).with_message(/abc/).strict
end
expect(matcher.failure_message_when_negated).to eq( expect(&assertion).to fail_with_message(message)
%{Expected exception to include /abc/ when attr is set to "xyz",\n} + end
%{got: "Attr is invalid"} end
)
end end
end end
end end