From 6ba8c9f1a1a544ba939fb2acbffaaa1fbc01041c Mon Sep 17 00:00:00 2001 From: Reade Harris Date: Tue, 16 Oct 2012 13:45:06 -0400 Subject: [PATCH] Use submatchers instead of inheritance for ValidatesNumericalityMatcher. --- lib/shoulda/matchers/active_model.rb | 2 + .../active_model/disallow_value_matcher.rb | 33 ++++++++++ .../active_model/only_integer_matcher.rb | 24 +++++++ .../validate_numericality_of_matcher.rb | 61 ++++++++++------- .../disallow_value_matcher_spec.rb | 65 +++++++++++++++++++ .../active_model/only_integer_matcher_spec.rb | 42 ++++++++++++ .../validate_numericality_of_matcher_spec.rb | 65 +++++++++++++++---- 7 files changed, 256 insertions(+), 36 deletions(-) create mode 100644 lib/shoulda/matchers/active_model/disallow_value_matcher.rb create mode 100644 lib/shoulda/matchers/active_model/only_integer_matcher.rb create mode 100644 spec/shoulda/active_model/disallow_value_matcher_spec.rb create mode 100644 spec/shoulda/active_model/only_integer_matcher_spec.rb diff --git a/lib/shoulda/matchers/active_model.rb b/lib/shoulda/matchers/active_model.rb index 5121fe34..0bd08160 100644 --- a/lib/shoulda/matchers/active_model.rb +++ b/lib/shoulda/matchers/active_model.rb @@ -3,6 +3,8 @@ 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/disallow_value_matcher' +require 'shoulda/matchers/active_model/only_integer_matcher' require 'shoulda/matchers/active_model/ensure_length_of_matcher' require 'shoulda/matchers/active_model/ensure_inclusion_of_matcher' require 'shoulda/matchers/active_model/ensure_exclusion_of_matcher' diff --git a/lib/shoulda/matchers/active_model/disallow_value_matcher.rb b/lib/shoulda/matchers/active_model/disallow_value_matcher.rb new file mode 100644 index 00000000..81127636 --- /dev/null +++ b/lib/shoulda/matchers/active_model/disallow_value_matcher.rb @@ -0,0 +1,33 @@ +module Shoulda # :nodoc: + module Matchers + module ActiveModel # :nodoc: + class DisallowValueMatcher # :nodoc: + def initialize(value) + @allow_matcher = Shoulda::Matchers::ActiveModel::AllowValueMatcher.new(value) + end + + def matches?(subject) + !@allow_matcher.matches?(subject) + end + + def for(attribute) + @allow_matcher.for(attribute) + self + end + + def with_message(message) + @allow_matcher.with_message(message) + self + end + + def failure_message + @allow_matcher.negative_failure_message + end + + def allowed_types + "" + end + end + end + end +end diff --git a/lib/shoulda/matchers/active_model/only_integer_matcher.rb b/lib/shoulda/matchers/active_model/only_integer_matcher.rb new file mode 100644 index 00000000..1827a522 --- /dev/null +++ b/lib/shoulda/matchers/active_model/only_integer_matcher.rb @@ -0,0 +1,24 @@ +module Shoulda # :nodoc: + module Matchers + module ActiveModel # :nodoc: + class OnlyIntegerMatcher # :nodoc: + def initialize(attribute) + @attribute = attribute + end + + def matches?(subject) + matcher = AllowValueMatcher.new(0.1).for(@attribute) + !matcher.matches?(subject) + end + + def with_message(message) + self + end + + def allowed_types + "integer" + end + end + end + end +end diff --git a/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb b/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb index 15418f8f..39d0e913 100644 --- a/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +++ b/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb @@ -17,55 +17,68 @@ module Shoulda # :nodoc: ValidateNumericalityOfMatcher.new(attr) end - class ValidateNumericalityOfMatcher < ValidationMatcher # :nodoc: + class ValidateNumericalityOfMatcher def initialize(attribute) - super(attribute) + @attribute = attribute @options = {} + @submatchers = [] + + add_disallow_value_matcher end def only_integer - @options[:only_integer] = true + only_integer_matcher = OnlyIntegerMatcher.new(@attribute) + add_submatcher(only_integer_matcher) + self end def with_message(message) - if message - @expected_message = message - end + @expected_message = message self end def matches?(subject) - super(subject) - disallows_non_integers? && disallows_text? + @subject = subject + set_expected_message_on_submatchers + submatchers_match? end def description - "only allow #{allowed_type} values for #{@attribute}" + "only allow #{allowed_types} values for #{@attribute}" + end + + def failure_message + @disallow_value_matcher.failure_message end private - def allowed_type - if @options[:only_integer] - "integer" - else - "numeric" - end + def add_disallow_value_matcher + @disallow_value_matcher = DisallowValueMatcher.new('abcd').for(@attribute) + add_submatcher(@disallow_value_matcher) end - def disallows_non_integers? - if @options[:only_integer] - message = @expected_message || :not_an_integer - disallows_value_of(0.1, message) - else - true - end + def add_submatcher(submatcher) + @submatchers << submatcher end - def disallows_text? + def set_expected_message_on_submatchers message = @expected_message || :not_a_number - disallows_value_of('abcd', message) + @submatchers.each { |matcher| matcher.with_message(message) } + end + + def submatchers_match? + @submatchers.all? { |matcher| matcher.matches?(@subject) } + end + + def allowed_types + allowed = ["numeric"] + submatcher_allowed_types + allowed.join(", ") + end + + def submatcher_allowed_types + @submatchers.map(&:allowed_types).reject { |type| type.empty? } end end end diff --git a/spec/shoulda/active_model/disallow_value_matcher_spec.rb b/spec/shoulda/active_model/disallow_value_matcher_spec.rb new file mode 100644 index 00000000..f5d02b98 --- /dev/null +++ b/spec/shoulda/active_model/disallow_value_matcher_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Shoulda::Matchers::ActiveModel::DisallowValueMatcher do + it "does not allow any types" do + matcher = Shoulda::Matchers::ActiveModel::DisallowValueMatcher.new("abcde") + matcher.allowed_types.should == "" + end + + context "an attribute with a format validation" do + before do + define_model :example, :attr => :string do + validates_format_of :attr, :with => /abc/ + end + @model = Example.new + end + + it "does not match if the value is allowed" do + matcher = new_matcher("abcde") + matcher.for(:attr) + matcher.matches?(@model).should be_false + end + + it "matches if the value is not allowed" do + matcher = new_matcher("xyz") + matcher.for(:attr) + matcher.matches?(@model).should be_true + end + end + + context "an attribute with a format validation and a custom message" do + before do + define_model :example, :attr => :string do + validates_format_of :attr, :with => /abc/, :message => 'good message' + end + @model = Example.new + end + + it "does not match if the value and message are both correct" do + matcher = new_matcher("abcde") + matcher.for(:attr).with_message('good message') + matcher.matches?(@model).should be_false + end + + it "delegates its failure message to its allow matcher's negative failure message" do + allow_matcher = stub_everything(negative_failure_message: "allow matcher failure") + Shoulda::Matchers::ActiveModel::AllowValueMatcher.stubs(:new).returns(allow_matcher) + + matcher = new_matcher("abcde") + matcher.for(:attr).with_message('good message') + matcher.matches?(@model) + + matcher.failure_message.should == "allow matcher failure" + end + + it "matches if the message is correct but the value is not" do + matcher = new_matcher("xyz") + matcher.for(:attr).with_message('good message') + matcher.matches?(@model).should be_true + end + end + + def new_matcher(value) + matcher = Shoulda::Matchers::ActiveModel::DisallowValueMatcher.new(value) + end +end diff --git a/spec/shoulda/active_model/only_integer_matcher_spec.rb b/spec/shoulda/active_model/only_integer_matcher_spec.rb new file mode 100644 index 00000000..fe097b55 --- /dev/null +++ b/spec/shoulda/active_model/only_integer_matcher_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Shoulda::Matchers::ActiveModel::OnlyIntegerMatcher do + context "given an attribute that only allows integer values" do + before do + define_model :example, :attr => :string do + validates_numericality_of :attr, { :only_integer => true } + end + @model = Example.new + end + + it "matches" do + matcher = new_matcher(:attr) + matcher.matches?(@model).should be_true + end + + it "allows integer types" do + matcher = new_matcher(:attr) + matcher.allowed_types.should == "integer" + end + + it "returns itself when given a message" do + matcher = new_matcher(:attr) + matcher.with_message("some message").should == matcher + end + end + + context "given an attribute that allows values other than integers" do + before do + @model = define_model(:example, :attr => :string).new + end + + it "does not match" do + matcher = new_matcher(:attr) + matcher.matches?(@model).should be_false + end + end + + def new_matcher(attribute) + matcher = Shoulda::Matchers::ActiveModel::OnlyIntegerMatcher.new(attribute) + end +end diff --git a/spec/shoulda/active_model/validate_numericality_of_matcher_spec.rb b/spec/shoulda/active_model/validate_numericality_of_matcher_spec.rb index 9b1a0bb5..899c0659 100644 --- a/spec/shoulda/active_model/validate_numericality_of_matcher_spec.rb +++ b/spec/shoulda/active_model/validate_numericality_of_matcher_spec.rb @@ -1,8 +1,12 @@ require 'spec_helper' describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher do + it "should state in its description that it allows only numeric values" do + matcher = Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher.new(:attr) + matcher.description.should == "only allow numeric values for attr" + end - context "a numeric attribute" do + context "given a numeric attribute" do before do define_model :example, :attr => :string do validates_numericality_of :attr @@ -11,28 +15,51 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher do end it "should only allow numeric values for that attribute" do - @model.should validate_numericality_of(:attr) + matcher = new_matcher(:attr) + matcher.matches?(@model).should be_true end it "should not override the default message with a blank" do - @model.should validate_numericality_of(:attr).with_message(nil) + matcher = new_matcher(:attr) + matcher.with_message(nil) + matcher.matches?(@model).should be_true + end + + it "should not enforce integer values for that attribute" do + matcher = new_matcher(:attr) + matcher.only_integer + matcher.matches?(@model).should be_false end end - context "a numeric attribute which must be integer" do + context "given a numeric attribute which must be integer" do before do define_model :example, :attr => :string do - validates_numericality_of :attr, { :only_integer => true } + validates_numericality_of :attr, { :only_integer => true } end @model = Example.new end - it "should only allow integer values for that attribute" do - @model.should validate_numericality_of(:attr).only_integer + it "allows integer values for that attribute" do + matcher = new_matcher(:attr) + matcher.only_integer + matcher.matches?(@model).should be_true + end + + it "does not allow non-integer values for that attribute" do + matcher = new_matcher(:attr) + matcher.only_integer + matcher.matches?(@model).should be_true + end + + it "should state in its description that it allows only integer values" do + matcher = new_matcher(:attr) + matcher.only_integer + matcher.description.should == "only allow numeric, integer values for attr" end end - context "a numeric attribute with a custom validation message" do + context "given a numeric attribute with a custom validation message" do before do define_model :example, :attr => :string do validates_numericality_of :attr, :message => 'custom' @@ -41,22 +68,36 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher do end it "should only allow numeric values for that attribute with that message" do - @model.should validate_numericality_of(:attr).with_message(/custom/) + matcher = new_matcher(:attr) + matcher.with_message(/custom/) + matcher.matches?(@model).should be_true end it "should not allow numeric values for that attribute with another message" do - @model.should_not validate_numericality_of(:attr) + matcher = new_matcher(:attr) + matcher.with_message(/wrong/) + matcher.matches?(@model).should be_false end end - context "a non-numeric attribute" do + context "given a non-numeric attribute" do before do @model = define_model(:example, :attr => :string).new end it "should not only allow numeric values for that attribute" do - @model.should_not validate_numericality_of(:attr) + matcher = new_matcher(:attr) + matcher.matches?(@model).should be_false + end + + it "should fail with the ActiveRecord :not_a_number message" do + matcher = new_matcher(:attr) + matcher.matches?(@model) + matcher.failure_message.should include 'Expected errors to include "is not a number"' end end + def new_matcher(attr) + Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher.new(attr) + end end