Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Support :strict option for validations #151

Merged
merged 7 commits into from

2 participants

@jferris
Owner

This adds support to all validation matchers for validates! and :strict => true. Strict validations raise an exception instead of adding messages to be displayed to the user.

@croaky
Owner

Implementation looks great. Looks like it could use some documentation.

@jferris
Owner

Good call. I'll add some docs today.

@jferris jferris merged commit fde078d into master

1 check failed

Details default The Travis build is in progress
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Sep 11, 2012
  1. @jferris

    Add AllowValueMatcher#strict to test strict validations

    jferris authored
    * Checks for exceptions raised from #validates! and :strict => true
Commits on Sep 12, 2012
  1. @jferris

    Add #strict option to validation matchers

    jferris authored
    * Allow verifying validates! and :strict => true
  2. @jferris
Commits on Sep 13, 2012
  1. @jferris
  2. @jferris

    Extract ExceptionMessageFinder from AllowValueMatcher

    jferris authored
    * Replaces repeated conditional with polymorphism
  3. @jferris

    Fix failure on 1.8.7

    jferris authored
Commits on Sep 14, 2012
  1. @jferris
This page is out of date. Refresh to see the latest.
View
13 lib/shoulda/matchers/active_model.rb
@@ -1,5 +1,7 @@
require 'shoulda/matchers/active_model/helpers'
require 'shoulda/matchers/active_model/validation_matcher'
+require 'shoulda/matchers/active_model/validation_message_finder'
+require 'shoulda/matchers/active_model/exception_message_finder'
require 'shoulda/matchers/active_model/allow_value_matcher'
require 'shoulda/matchers/active_model/ensure_length_of_matcher'
require 'shoulda/matchers/active_model/ensure_inclusion_of_matcher'
@@ -27,8 +29,19 @@ module Matchers
# end
# it { should allow_value("(123) 456-7890").for(:phone_number) }
# it { should_not allow_mass_assignment_of(:password) }
+ # it { should allow_value('Activated', 'Pending').for(:status).strict }
+ # it { should_not allow_value('Amazing').for(:status).strict }
# end
#
+ # These tests work with the following model:
+ #
+ # class User < ActiveRecord::Base
+ # validates_presence_of :name
+ # validates_presence_of :phone_number
+ # validates_format_of :phone_number, :with => /\\(\\d{3}\\) \\d{3}\\-\\d{4}/
+ # validates_inclusion_of :status, :in => %w(Activated Pending), :strict => true
+ # attr_accessible :name, :phone_number
+ # end
module ActiveModel
end
end
View
66 lib/shoulda/matchers/active_model/allow_value_matcher.rb
@@ -11,6 +11,9 @@ module ActiveModel # :nodoc:
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. If omitted,
# the test looks for any errors in <tt>errors.on(:attribute)</tt>.
+ # * <tt>strict</tt> - expects the model to raise an exception when the
+ # validation fails rather than adding to the errors collection. Used for
+ # testing `validates!` and the `:strict => true` validation options.
#
# Example:
# it { should_not allow_value('bad').for(:isbn) }
@@ -29,6 +32,7 @@ class AllowValueMatcher # :nodoc:
def initialize(*values)
@values_to_match = values
+ @message_finder_factory = ValidationMessageFinder
@options = {}
end
@@ -42,6 +46,11 @@ def with_message(message)
self
end
+ def strict
+ @message_finder_factory = ExceptionMessageFinder
+ self
+ end
+
def matches?(instance)
@instance = instance
@values_to_match.none? do |value|
@@ -60,30 +69,29 @@ def negative_failure_message
end
def description
- "allow #{@attribute} to be set to #{allowed_values}"
+ message_finder.allow_description(allowed_values)
end
private
def errors_match?
- if @instance.valid?
- false
+ has_messages? && errors_for_attribute_match?
+ end
+
+ def has_messages?
+ message_finder.has_messages?
+ end
+
+ def errors_for_attribute_match?
+ if expected_message
+ @matched_error = errors_match_regexp? || errors_match_string?
else
- if expected_message
- @matched_error = errors_match_regexp? || errors_match_string?
- else
- errors_for_attribute.compact.any?
- end
+ errors_for_attribute.compact.any?
end
end
def errors_for_attribute
- if @instance.errors.respond_to?(:[])
- errors = @instance.errors[@attribute]
- else
- errors = @instance.errors.on(@attribute)
- end
- Array.wrap(errors)
+ message_finder.messages
end
def errors_match_regexp?
@@ -100,15 +108,15 @@ def errors_match_string?
def expectation
includes_expected_message = expected_message ? "to include #{expected_message.inspect}" : ''
- ["errors", includes_expected_message, "when #{@attribute} is set to #{@value.inspect}"].join(' ')
+ [error_source, includes_expected_message, "when #{@attribute} is set to #{@value.inspect}"].join(' ')
+ end
+
+ def error_source
+ message_finder.source_description
end
def error_description
- if @instance.errors.empty?
- "no errors"
- else
- "errors: #{pretty_error_messages(@instance)}"
- end
+ message_finder.messages_description
end
def allowed_values
@@ -122,16 +130,32 @@ def allowed_values
def expected_message
if @options.key?(:expected_message)
if Symbol === @options[:expected_message]
- default_error_message(@options[:expected_message], :model_name => model_name, :attribute => @attribute)
+ default_expected_message
else
@options[:expected_message]
end
end
end
+ def default_expected_message
+ message_finder.expected_message_from(default_attribute_message)
+ end
+
+ def default_attribute_message
+ default_error_message(
+ @options[:expected_message],
+ :model_name => model_name,
+ :attribute => @attribute
+ )
+ end
+
def model_name
@instance.class.to_s.underscore
end
+
+ def message_finder
+ @message_finder ||= @message_finder_factory.new(@instance, @attribute)
+ end
end
end
end
View
58 lib/shoulda/matchers/active_model/exception_message_finder.rb
@@ -0,0 +1,58 @@
+module Shoulda
+ module Matchers
+ module ActiveModel
+
+ # Finds message information from exceptions thrown by #valid?
+ class ExceptionMessageFinder
+ def initialize(instance, attribute)
+ @instance = instance
+ @attribute = attribute
+ end
+
+ def allow_description(allowed_values)
+ "doesn't raise when #{@attribute} is set to #{allowed_values}"
+ end
+
+ def messages_description
+ if has_messages?
+ messages.join
+ else
+ 'no exception'
+ end
+ end
+
+ def has_messages?
+ messages.any?
+ end
+
+ def messages
+ @messages ||= validate_and_rescue
+ end
+
+ def source_description
+ 'exception'
+ end
+
+ def expected_message_from(attribute_message)
+ "#{human_attribute_name} #{attribute_message}"
+ end
+
+ private
+
+ def validate_and_rescue
+ @instance.valid?
+ []
+ rescue ::ActiveModel::StrictValidationFailed => exception
+ [exception.message]
+ end
+
+ def human_attribute_name
+ @instance.class.human_attribute_name(@attribute)
+ end
+ end
+
+ end
+ end
+end
+
+
View
35 lib/shoulda/matchers/active_model/validation_matcher.rb
@@ -6,6 +6,12 @@ class ValidationMatcher # :nodoc:
def initialize(attribute)
@attribute = attribute
+ @strict = false
+ end
+
+ def strict
+ @strict = true
+ self
end
def negative_failure_message
@@ -20,10 +26,8 @@ def matches?(subject)
private
def allows_value_of(value, message = nil)
- allow = AllowValueMatcher.
- new(value).
- for(@attribute).
- with_message(message)
+ allow = allow_value_matcher(value, message)
+
if allow.matches?(@subject)
@negative_failure_message = allow.failure_message
true
@@ -34,10 +38,8 @@ def allows_value_of(value, message = nil)
end
def disallows_value_of(value, message = nil)
- disallow = AllowValueMatcher.
- new(value).
- for(@attribute).
- with_message(message)
+ disallow = allow_value_matcher(value, message)
+
if disallow.matches?(@subject)
@failure_message = disallow.negative_failure_message
false
@@ -46,6 +48,23 @@ def disallows_value_of(value, message = nil)
true
end
end
+
+ def allow_value_matcher(value, message)
+ matcher = AllowValueMatcher.
+ new(value).
+ for(@attribute).
+ with_message(message)
+
+ if strict?
+ matcher.strict
+ else
+ matcher
+ end
+ end
+
+ def strict?
+ @strict
+ end
end
end
end
View
69 lib/shoulda/matchers/active_model/validation_message_finder.rb
@@ -0,0 +1,69 @@
+module Shoulda
+ module Matchers
+ module ActiveModel
+
+ # Finds message information from a model's #errors method.
+ class ValidationMessageFinder
+ include Helpers
+
+ def initialize(instance, attribute)
+ @instance = instance
+ @attribute = attribute
+ end
+
+ def allow_description(allowed_values)
+ "allow #{@attribute} to be set to #{allowed_values}"
+ end
+
+ def expected_message_from(attribute_message)
+ attribute_message
+ end
+
+ def has_messages?
+ errors.present?
+ end
+
+ def source_description
+ 'errors'
+ end
+
+ def messages_description
+ if errors.empty?
+ "no errors"
+ else
+ "errors: #{pretty_error_messages(validated_instance)}"
+ end
+ end
+
+ def messages
+ Array.wrap(messages_for_attribute)
+ end
+
+ private
+
+ def messages_for_attribute
+ if errors.respond_to?(:[])
+ errors[@attribute]
+ else
+ errors.on(@attribute)
+ end
+ end
+
+ def errors
+ validated_instance.errors
+ end
+
+ def validated_instance
+ @validated_instance ||= validate_instance
+ end
+
+ def validate_instance
+ @instance.valid?
+ @instance
+ end
+ end
+
+ end
+ end
+end
+
View
32 spec/shoulda/active_model/allow_value_matcher_spec.rb
@@ -88,4 +88,36 @@
end.should raise_error(ArgumentError, /at least one argument/)
end
end
+
+ if Rails::VERSION::STRING.to_f >= 3.2
+ context "an attribute with a strict format validation" do
+ let(:model) do
+ define_model :example, :attr => :string do
+ validates_format_of :attr, :with => /abc/, :strict => true
+ end.new
+ end
+
+ it "strictly rejects a bad value" do
+ model.should_not allow_value("xyz").for(:attr).strict
+ end
+
+ it "strictly allows a bad value with a different message" do
+ model.should allow_value("xyz").for(:attr).with_message(/abc/).strict
+ end
+
+ it "describes itself" do
+ allow_value("xyz").for(:attr).strict.description.
+ should == %{doesn't raise when attr is set to "xyz"}
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = allow_value("xyz").for(:attr).strict.with_message(/abc/)
+ matcher.matches?(model)
+ matcher.negative_failure_message.
+ should == 'Expected exception to include /abc/ ' +
+ 'when attr is set to "xyz", got Attr is invalid'
+ end
+ end
+ end
+
end
View
18 spec/shoulda/active_model/ensure_inclusion_of_matcher_spec.rb
@@ -120,4 +120,22 @@ def custom_validation
@model.should_not ensure_inclusion_of(:attr).in_array(['one', 'two']).allow_nil(false)
end
end
+
+ if Rails::VERSION::STRING.to_f >= 3.2
+ context "a strict attribute which must be included in a range" do
+ before do
+ @model = define_model(:example, :attr => :integer) do
+ validates_inclusion_of :attr, :in => 2..5, :strict => true
+ end.new
+ end
+
+ it "should accept ensuring the correct range" do
+ @model.should ensure_inclusion_of(:attr).in_range(2..5).strict
+ end
+
+ it "should not accept ensuring another range" do
+ @model.should_not ensure_inclusion_of(:attr).in_range(2..6).strict
+ end
+ end
+ end
end
View
112 spec/shoulda/active_model/exception_message_finder_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+
+describe Shoulda::Matchers::ActiveModel::ExceptionMessageFinder do
+ if Rails::VERSION::STRING.to_f >= 3.2
+ context '#allow_description' do
+ it 'describes its attribute' do
+ finder = build_finder(:attribute => :attr)
+
+ description = finder.allow_description('allowed values')
+
+ description.should == "doesn't raise when attr is set to allowed values"
+ end
+ end
+
+ context '#expected_message_from' do
+ it 'returns the message with the attribute name prefixed' do
+ finder = build_finder(:attribute => :attr)
+
+ message = finder.expected_message_from('some message')
+
+ message.should == 'Attr some message'
+ end
+ end
+
+ context '#has_messages?' do
+ it 'has messages when some validations fail' do
+ finder = build_finder(:format => /abc/, :value => 'xyz')
+
+ result = finder.has_messages?
+
+ result.should be_true
+ end
+
+ it 'has no messages when all validations pass' do
+ finder = build_finder(:format => /abc/, :value => 'abc')
+
+ result = finder.has_messages?
+
+ result.should be_false
+ end
+ end
+
+ context '#messages' do
+ it 'returns errors for the given attribute' do
+ finder = build_finder(
+ :attribute => :attr,
+ :format => /abc/,
+ :value => 'xyz'
+ )
+
+ messages = finder.messages
+
+ messages.should == ['Attr is invalid']
+ end
+ end
+
+ context '#messages_description' do
+ it 'describes errors for the given attribute' do
+ finder = build_finder(
+ :attribute => :attr,
+ :format => /abc/,
+ :value => 'xyz'
+ )
+
+ description = finder.messages_description
+
+ description.should == 'Attr is invalid'
+ end
+
+ it 'describes errors when there are none' do
+ finder = build_finder(:format => /abc/, :value => 'abc')
+
+ description = finder.messages_description
+
+ description.should == 'no exception'
+ end
+ end
+
+ context '#source_description' do
+ it 'describes the source of its messages' do
+ finder = build_finder
+
+ description = finder.source_description
+
+ description.should == 'exception'
+ end
+ end
+ end
+
+ def build_finder(arguments = {})
+ arguments[:attribute] ||= :attr
+ instance = build_instance_validating(
+ arguments[:attribute],
+ arguments[:format] || /abc/,
+ arguments[:value] || 'abc'
+ )
+ Shoulda::Matchers::ActiveModel::ExceptionMessageFinder.new(
+ instance,
+ arguments[:attribute]
+ )
+ end
+
+ def build_instance_validating(attribute, format, value)
+ model_class = define_model(:example, attribute => :string) do
+ attr_accessible attribute
+ validates_format_of attribute, :with => format, :strict => true
+ end
+
+ model_class.new(attribute => value)
+ end
+end
+
View
15 spec/shoulda/active_model/validate_presence_of_matcher_spec.rb
@@ -117,4 +117,19 @@
end
end
+ if Rails::VERSION::STRING.to_f >= 3.2
+ context "a strictly required attribute" do
+ before do
+ define_model :example, :attr => :string do
+ validates_presence_of :attr, :strict => true
+ end
+ @model = Example.new
+ end
+
+ it "should require a value" do
+ @model.should validate_presence_of(:attr).strict
+ end
+ end
+ end
+
end
View
107 spec/shoulda/active_model/validation_message_finder_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe Shoulda::Matchers::ActiveModel::ValidationMessageFinder do
+ context '#allow_description' do
+ it 'describes its attribute' do
+ finder = build_finder(:attribute => :attr)
+
+ description = finder.allow_description('allowed values')
+
+ description.should == 'allow attr to be set to allowed values'
+ end
+ end
+
+ context '#expected_message_from' do
+ it 'returns the message as-is' do
+ finder = build_finder
+
+ message = finder.expected_message_from('some message')
+
+ message.should == 'some message'
+ end
+ end
+
+ context '#has_messages?' do
+ it 'has messages when some validations fail' do
+ finder = build_finder(:format => /abc/, :value => 'xyz')
+
+ result = finder.has_messages?
+
+ result.should be_true
+ end
+
+ it 'has no messages when all validations pass' do
+ finder = build_finder(:format => /abc/, :value => 'abc')
+
+ result = finder.has_messages?
+
+ result.should be_false
+ end
+ end
+
+ context '#messages' do
+ it 'returns errors for the given attribute' do
+ finder = build_finder(:format => /abc/, :value => 'xyz')
+
+ messages = finder.messages
+
+ messages.should == ['is invalid']
+ end
+ end
+
+ context '#messages_description' do
+ it 'describes errors for the given attribute' do
+ value = 'xyz'
+ finder = build_finder(
+ :attribute => :attr,
+ :format => /abc/,
+ :value => 'xyz'
+ )
+
+ description = finder.messages_description
+
+ expected_messages = ['attr is invalid ("xyz")']
+ description.should == "errors: #{expected_messages}"
+ end
+
+ it 'describes errors when there are none' do
+ finder = build_finder(:format => /abc/, :value => 'abc')
+
+ description = finder.messages_description
+
+ description.should == 'no errors'
+ end
+ end
+
+ context '#source_description' do
+ it 'describes the source of its messages' do
+ finder = build_finder
+
+ description = finder.source_description
+
+ description.should == 'errors'
+ end
+ end
+
+ def build_finder(arguments = {})
+ arguments[:attribute] ||= :attr
+ instance = build_instance_validating(
+ arguments[:attribute],
+ arguments[:format] || /abc/,
+ arguments[:value] || 'abc'
+ )
+ Shoulda::Matchers::ActiveModel::ValidationMessageFinder.new(
+ instance,
+ arguments[:attribute]
+ )
+ end
+
+ def build_instance_validating(attribute, format, value)
+ model_class = define_model(:example, attribute => :string) do
+ attr_accessible attribute
+ validates_format_of attribute, :with => format
+ end
+
+ model_class.new(attribute => value)
+ end
+end
Something went wrong with that request. Please try again.