diff --git a/Changelog.md b/Changelog.md index 30e9e4f78..57c389bea 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,6 +3,10 @@ Deprecations +* Deprecate `have`, `have_at_least` and `have_at_most`. You can continue using those + matchers through https://github.com/rspec/rspec-collection_matchers, or + you can rewrite your expectations with something like + `expect(your_object.size).to eq(num)` (Hugo Baraúna) * Deprecate `be_xyz` predicate matcher when `xyz?` is a private method (Jon Rowe). * Deprecate `be_true`/`be_false` in favour of `be_truthy`/`be_falsey` diff --git a/lib/rspec/matchers/built_in/have.rb b/lib/rspec/matchers/built_in/have.rb index 1fe90a29c..e0b63cefa 100644 --- a/lib/rspec/matchers/built_in/have.rb +++ b/lib/rspec/matchers/built_in/have.rb @@ -11,7 +11,11 @@ def initialize(expected, relativity=:exactly) else expected end @relativity = relativity + @actual = @collection_name = @plural_collection_name = nil + @target_owns_a_collection = false + @negative_expectation = false + @expectation_format_method = "to" end def relativities @@ -29,13 +33,20 @@ def matches?(collection_or_owner) for query_method in QUERY_METHODS next unless collection.respond_to?(query_method) @actual = collection.__send__(query_method) - break unless @actual.nil? + + if @actual + print_deprecation_message(query_method) + break + end end + raise not_a_collection if @actual.nil? else query_method = determine_query_method(collection) raise not_a_collection unless query_method @actual = collection.__send__(query_method) + + print_deprecation_message(query_method) end case @relativity when :at_least then @actual >= @expected @@ -45,10 +56,18 @@ def matches?(collection_or_owner) end alias == matches? + def does_not_match?(collection_or_owner) + @negative_expectation = true + @expectation_format_method = "to_not" + !matches?(collection_or_owner) + end + def determine_collection(collection_or_owner) if collection_or_owner.respond_to?(@collection_name) + @target_owns_a_collection = true collection_or_owner.__send__(@collection_name, *@args, &@block) elsif (@plural_collection_name && collection_or_owner.respond_to?(@plural_collection_name)) + @target_owns_a_collection = true collection_or_owner.__send__(@plural_collection_name, *@args, &@block) elsif determine_query_method(collection_or_owner) collection_or_owner @@ -118,6 +137,83 @@ def relative_expectation def enumerator_class RUBY_VERSION < '1.9' ? Enumerable::Enumerator : Enumerator end + + def print_deprecation_message(query_method) + deprecation_message = "the rspec-collection_matchers gem " + deprecation_message << "or replace your expectation with something like " + deprecation_message << "`expect(#{cardinality_expression(query_method)}).#{expectation_format_method} #{suggested_matcher_expression}`" + + RSpec.deprecate("`#{expectation_expression(query_method)}`", :replacement => deprecation_message) + end + + def expectation_expression(query_method) + if @negative_expectation + RSpec::Expectations::Syntax.negative_expression(target_expression, original_matcher_expression) + else + RSpec::Expectations::Syntax.positive_expression(target_expression, original_matcher_expression) + end + end + + def target_expression + if @target_owns_a_collection + 'collection_owner' + else + 'collection' + end + end + + def original_matcher_expression + "#{matcher_method}(#{@expected}).#{@collection_name}" + end + + def expectation_format_method + if @relativity == :exactly + @expectation_format_method + else + "to" + end + end + + def cardinality_expression(query_method) + expression = "#{target_expression}." + expression << "#{@collection_name}." if @target_owns_a_collection + expression << String(query_method) + end + + def suggested_matcher_expression + send("suggested_matcher_expression_for_#{@relativity}") + end + + def suggested_matcher_expression_for_exactly + "eq(#{@expected})" + end + + def suggested_matcher_expression_for_at_most + if @negative_expectation + "be > #{@expected}" + else + "be <= #{@expected}" + end + end + + def suggested_matcher_expression_for_at_least + if @negative_expectation + "be < #{@expected}" + else + "be >= #{@expected}" + end + end + + def matcher_method + case @relativity + when :exactly + "have" + when :at_most + "have_at_most" + when :at_least + "have_at_least" + end + end end end end diff --git a/spec/rspec/matchers/have_spec.rb b/spec/rspec/matchers/have_spec.rb index 98945f85c..afd19c44a 100644 --- a/spec/rspec/matchers/have_spec.rb +++ b/spec/rspec/matchers/have_spec.rb @@ -452,4 +452,301 @@ def array.send; :sent; end end end end + + context "deprecations for the have matcher" do + it "has the correct call site in the deprecation message" do + expect_deprecation_with_call_site(__FILE__, __LINE__ + 1) + expect([1, 2, 3]).to have(3).items + end + + context "when the target is a collection" do + it "prints a specific message for the positive expectation format" do + expectation_expression = "expect(collection).to have(3).items" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection.size).to eq(3)`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + expect([1, 2, 3]).to have(3).items + end + + it "prints a specific message for the negative expectation format" do + expectation_expression = "expect(collection).not_to have(4).items" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection.size).to_not eq(4)`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + expect([1, 2, 3]).to_not have(4).items + end + end + + context "when the target owns a collection" do + class BagOfWords + attr_reader :words + + def initialize(words) + @words = words + end + end + + it "prints a specific message for the positive expectation format" do + expectation_expression = "expect(collection_owner).to have(3).words" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection_owner.words.size).to eq(3)`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + target = BagOfWords.new(%w[foo bar baz]) + expect(target).to have(3).words + end + + it "prints a specific message for the negative expectation format" do + expectation_expression = "expect(collection_owner).not_to have(4).words" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection_owner.words.size).to_not eq(4)`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + target = BagOfWords.new(%w[foo bar baz]) + expect(target).to_not have(4).words + end + end + + context "when the target is an enumerator" do + it "prints a specific message for the positive expectation format" do + target = %w[a b c].to_enum(:each) + + expectation_expression = "expect(collection).to have(3).letters" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection.count).to eq(3)`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + expect(target).to have(3).letters + end + + it "prints a specific message for the negative expectation format" do + target = %w[a b c].to_enum(:each) + + expectation_expression = "expect(collection).not_to have(4).letters" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection.count).to_not eq(4)`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + expect(target).to_not have(4).letters + end + end + end + + context "deprecations for the have_at_most matcher" do + it "has the correct call site in the deprecation message" do + expect_deprecation_with_call_site(__FILE__, __LINE__ + 1) + expect([1, 2, 3]).to have_at_most(3).items + end + + context "when the target is a collection" do + it "prints a specific message for the positive expectation format" do + expectation_expression = "expect(collection).to have_at_most(3).items" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection.size).to be <= 3`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + expect([1, 2, 3]).to have_at_most(3).items + end + + it "prints a specific message for the negative expectation format" do + expectation_expression = "expect(collection).not_to have_at_most(2).items" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection.size).to be > 2`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + expect([1, 2, 3]).to_not have_at_most(2).items + end + end + + context "when the target owns a collection" do + class BagOfWords + attr_reader :words + + def initialize(words) + @words = words + end + end + + it "prints a specific message for the positive expectation format" do + expectation_expression = "expect(collection_owner).to have_at_most(3).words" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection_owner.words.size).to be <= 3`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + target = BagOfWords.new(%w[foo bar baz]) + expect(target).to have_at_most(3).words + end + + it "prints a specific message for the negative expectation format" do + expectation_expression = "expect(collection_owner).not_to have_at_most(2).words" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection_owner.words.size).to be > 2`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + target = BagOfWords.new(%w[foo bar baz]) + expect(target).to_not have_at_most(2).words + end + end + + context "when the target is an enumerator" do + it "prints a specific message for the positive expectation format" do + target = %w[a b c].to_enum(:each) + + expectation_expression = "expect(collection).to have_at_most(3).letters" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection.count).to be <= 3`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + expect(target).to have_at_most(3).letters + end + + it "prints a specific message for the negative expectation format" do + target = %w[a b c].to_enum(:each) + + expectation_expression = "expect(collection).not_to have_at_most(2).letters" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection.count).to be > 2`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + expect(target).to_not have_at_most(2).letters + end + end + end + + context "deprecations for the have_at_least matcher" do + it "has the correct call site in the deprecation message" do + expect_deprecation_with_call_site(__FILE__, __LINE__ + 1) + expect([1, 2, 3]).to have_at_least(3).items + end + + context "when the target is a collection" do + it "prints a specific message for the positive expectation format" do + expectation_expression = "expect(collection).to have_at_least(3).items" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection.size).to be >= 3`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + expect([1, 2, 3]).to have_at_least(3).items + end + + it "prints a specific message for the negative expectation format" do + expectation_expression = "expect(collection).not_to have_at_least(4).items" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection.size).to be < 4`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + expect([1, 2, 3]).to_not have_at_least(4).items + end + end + + context "when the target owns a collection" do + class BagOfWords + attr_reader :words + + def initialize(words) + @words = words + end + end + + it "prints a specific message for the positive expectation format" do + expectation_expression = "expect(collection_owner).to have_at_least(3).words" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection_owner.words.size).to be >= 3`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + target = BagOfWords.new(%w[foo bar baz]) + expect(target).to have_at_least(3).words + end + + it "prints a specific message for the negative expectation format" do + expectation_expression = "expect(collection_owner).not_to have_at_least(4).words" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection_owner.words.size).to be < 4`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + target = BagOfWords.new(%w[foo bar baz]) + expect(target).to_not have_at_least(4).words + end + end + + context "when the target is an enumerator" do + it "prints a specific message for the positive expectation format" do + target = %w[a b c].to_enum(:each) + + expectation_expression = "expect(collection).to have_at_least(3).letters" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection.count).to be >= 3`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + expect(target).to have_at_least(3).letters + end + + it "prints a specific message for the negative expectation format" do + target = %w[a b c].to_enum(:each) + + expectation_expression = "expect(collection).not_to have_at_least(4).letters" + + message = "the rspec-collection_matchers gem " + + "or replace your expectation with something like " + + "`expect(collection.count).to be < 4`" + + expect(RSpec).to receive(:deprecate).with("`#{expectation_expression}`", :replacement => message) + + expect(target).to_not have_at_least(4).letters + end + end + end end