Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Support :strict option for validations #151

Merged
merged 7 commits into from

2 participants

Joe Ferris Dan Croak
Joe Ferris
Admin

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.

Dan Croak
Admin

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

Joe Ferris
Admin

Good call. I'll add some docs today.

Joe Ferris jferris merged commit fde078d into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Sep 11, 2012
  1. Joe Ferris

    Add AllowValueMatcher#strict to test strict validations

    jferris authored
    * Checks for exceptions raised from #validates! and :strict => true
  2. Joe Ferris

    Add #strict option to validation matchers

    jferris authored
    * Allow verifying validates! and :strict => true
Commits on Sep 12, 2012
  1. Joe Ferris
  2. Joe Ferris
  3. Joe Ferris

    Extract ExceptionMessageFinder from AllowValueMatcher

    jferris authored
    * Replaces repeated conditional with polymorphism
  4. Joe Ferris

    Fix failure on 1.8.7

    jferris authored
Commits on Sep 14, 2012
  1. Joe Ferris
This page is out of date. Refresh to see the latest.
13 lib/shoulda/matchers/active_model.rb
View
@@ -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
66 lib/shoulda/matchers/active_model/allow_value_matcher.rb
View
@@ -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
58 lib/shoulda/matchers/active_model/exception_message_finder.rb
View
@@ -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
+
+
35 lib/shoulda/matchers/active_model/validation_matcher.rb
View
@@ -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
69 lib/shoulda/matchers/active_model/validation_message_finder.rb
View
@@ -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
+
32 spec/shoulda/active_model/allow_value_matcher_spec.rb
View
@@ -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
18 spec/shoulda/active_model/ensure_inclusion_of_matcher_spec.rb
View
@@ -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
112 spec/shoulda/active_model/exception_message_finder_spec.rb
View
@@ -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
+
15 spec/shoulda/active_model/validate_presence_of_matcher_spec.rb
View
@@ -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
107 spec/shoulda/active_model/validation_message_finder_spec.rb
View
@@ -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.