diff --git a/Changelog.md b/Changelog.md index d93da7cd2..c30aa1ad6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,7 +4,8 @@ Breaking Changes: * Ruby < 2.3 is no longer supported. (Phil Pirozhkov, #1231) -* Extract monkey-patching `should*` syntax and globally-exposed DSL. (Phil Pirozhkov, #1245) +* Extract `should*` (both monkey-patching and non-monkey-patching) syntax and + globally-exposed DSL. (Phil Pirozhkov, #1245) Enhancements: diff --git a/README.md b/README.md index e7a4e38bf..03c3ffcae 100644 --- a/README.md +++ b/README.md @@ -216,17 +216,6 @@ expect( ).to match([a_hash_including(:a => 'hash'), a_hash_including(:a => 'another')]) ``` -## `should` syntax - -In addition to the `expect` syntax, rspec-expectations continues to support the -non-monkey patching `should` syntax: - -```ruby -subject(:number) { 4 } -it { should eq(4) } -it { should be > 3 } -it { should_not be_odd } -``` ## Compound Matcher Expressions diff --git a/features/.nav b/features/.nav index 66eba770b..e14a50cbb 100644 --- a/features/.nav +++ b/features/.nav @@ -16,7 +16,6 @@ - have_attributes.feature - include.feature - match.feature - - operators.feature - raise_error.feature - respond_to.feature - satisfy.feature diff --git a/features/customized_message.feature b/features/customized_message.feature index c96a3ea0f..ec40f8137 100644 --- a/features/customized_message.feature +++ b/features/customized_message.feature @@ -1,8 +1,7 @@ Feature: customized message RSpec tries to provide useful failure messages, but for cases in which you want more - specific information, you can define your own message right in the example.This works for - any matcher _other than the operator matchers_. + specific information, you can define your own message right in the example. Scenario: customize failure message Given a file named "example_spec.rb" with: diff --git a/lib/rspec/expectations/handler.rb b/lib/rspec/expectations/handler.rb index a8f7c0531..3ff9b129f 100644 --- a/lib/rspec/expectations/handler.rb +++ b/lib/rspec/expectations/handler.rb @@ -46,8 +46,6 @@ def self.handle_failure(matcher, message, failure_message_method) class PositiveExpectationHandler def self.handle_matcher(actual, initial_matcher, custom_message=nil, &block) ExpectationHelper.with_matcher(self, initial_matcher, custom_message) do |matcher| - return ::RSpec::Matchers::BuiltIn::PositiveOperatorMatcher.new(actual) unless initial_matcher - match_result = matcher.matches?(actual, &block) if custom_message && match_result.respond_to?(:error_generator) match_result.error_generator.opts[:message] = custom_message @@ -74,8 +72,6 @@ def self.opposite_should_method class NegativeExpectationHandler def self.handle_matcher(actual, initial_matcher, custom_message=nil, &block) ExpectationHelper.with_matcher(self, initial_matcher, custom_message) do |matcher| - return ::RSpec::Matchers::BuiltIn::NegativeOperatorMatcher.new(actual) unless initial_matcher - negated_match_result = does_not_match?(matcher, actual, &block) if custom_message && negated_match_result.respond_to?(:error_generator) negated_match_result.error_generator.opts[:message] = custom_message diff --git a/lib/rspec/matchers.rb b/lib/rspec/matchers.rb index e25cdac62..444c83f11 100644 --- a/lib/rspec/matchers.rb +++ b/lib/rspec/matchers.rb @@ -501,9 +501,6 @@ def change(receiver=nil, message=nil, &block) # This works for collections. Pass in multiple args and it will only # pass if all args are found in collection. # - # @note This is also available using the `=~` operator with `should`, - # but `=~` is not supported with `expect`. - # # @example # expect([1, 2, 3]).to contain_exactly(1, 2, 3) # expect([1, 2, 3]).to contain_exactly(1, 3, 2) diff --git a/lib/rspec/matchers/built_in.rb b/lib/rspec/matchers/built_in.rb index e6237ff08..4cd7dfee9 100644 --- a/lib/rspec/matchers/built_in.rb +++ b/lib/rspec/matchers/built_in.rb @@ -35,10 +35,7 @@ module BuiltIn autoload :Include, 'rspec/matchers/built_in/include' autoload :All, 'rspec/matchers/built_in/all' autoload :Match, 'rspec/matchers/built_in/match' - autoload :NegativeOperatorMatcher, 'rspec/matchers/built_in/operators' - autoload :OperatorMatcher, 'rspec/matchers/built_in/operators' autoload :Output, 'rspec/matchers/built_in/output' - autoload :PositiveOperatorMatcher, 'rspec/matchers/built_in/operators' autoload :RaiseError, 'rspec/matchers/built_in/raise_error' autoload :RespondTo, 'rspec/matchers/built_in/respond_to' autoload :Satisfy, 'rspec/matchers/built_in/satisfy' diff --git a/lib/rspec/matchers/built_in/operators.rb b/lib/rspec/matchers/built_in/operators.rb deleted file mode 100644 index 188eb14a9..000000000 --- a/lib/rspec/matchers/built_in/operators.rb +++ /dev/null @@ -1,128 +0,0 @@ -require 'rspec/support' - -module RSpec - module Matchers - module BuiltIn - # @api private - # Provides the implementation for operator matchers. - # Not intended to be instantiated directly. - # Only available for use with `should` one-liner syntax. - class OperatorMatcher - class << self - # @private - def registry - @registry ||= {} - end - - # @private - def register(klass, operator, matcher) - registry[klass] ||= {} - registry[klass][operator] = matcher - end - - # @private - def unregister(klass, operator) - registry[klass] && registry[klass].delete(operator) - end - - # @private - def get(klass, operator) - klass.ancestors.each do |ancestor| - matcher = registry[ancestor] && registry[ancestor][operator] - return matcher if matcher - end - - nil - end - end - - register Enumerable, '=~', BuiltIn::ContainExactly - - def initialize(actual) - @actual = actual - end - - # @private - def self.use_custom_matcher_or_delegate(operator) - define_method(operator) do |expected| - if !has_non_generic_implementation_of?(operator) && (matcher = OperatorMatcher.get(@actual.class, operator)) - @actual.__send__(::RSpec::Matchers.last_expectation_handler.should_method, matcher.new(expected)) - else - eval_match(@actual, operator, expected) - end - end - - negative_operator = operator.sub(/^=/, '!') - if negative_operator != operator && respond_to?(negative_operator) - define_method(negative_operator) do |_expected| - opposite_should = ::RSpec::Matchers.last_expectation_handler.opposite_should_method - raise "RSpec does not support `#{::RSpec::Matchers.last_expectation_handler.should_method} #{negative_operator} expected`. " \ - "Use `#{opposite_should} #{operator} expected` instead." - end - end - end - - ['==', '===', '=~', '>', '>=', '<', '<='].each do |operator| - use_custom_matcher_or_delegate operator - end - - # @private - def fail_with_message(message) - RSpec::Expectations.fail_with(message, @expected, @actual) - end - - # @api private - # @return [String] - def description - "#{@operator} #{RSpec::Support::ObjectFormatter.format(@expected)}" - end - - private - - def has_non_generic_implementation_of?(op) - Support.method_handle_for(@actual, op).owner != ::Kernel - rescue NameError - false - end - - def eval_match(actual, operator, expected) - ::RSpec::Matchers.last_matcher = self - @operator, @expected = operator, expected - __delegate_operator(actual, operator, expected) - end - end - - # @private - # Handles operator matcher for positive expectations - class PositiveOperatorMatcher < OperatorMatcher - def __delegate_operator(actual, operator, expected) - if actual.__send__(operator, expected) - true - else - expected_formatted = RSpec::Support::ObjectFormatter.format(expected) - actual_formatted = RSpec::Support::ObjectFormatter.format(actual) - - if ['==', '===', '=~'].include?(operator) - fail_with_message("expected: #{expected_formatted}\n got: #{actual_formatted} (using #{operator})") - else - fail_with_message("expected: #{operator} #{expected_formatted}\n got: #{operator.gsub(/./, ' ')} #{actual_formatted}") - end - end - end - end - - # @private - # Handles operator matcher for negative expectations - class NegativeOperatorMatcher < OperatorMatcher - def __delegate_operator(actual, operator, expected) - return false unless actual.__send__(operator, expected) - - expected_formatted = RSpec::Support::ObjectFormatter.format(expected) - actual_formatted = RSpec::Support::ObjectFormatter.format(actual) - - fail_with_message("expected not: #{operator} #{expected_formatted}\n got: #{operator.gsub(/./, ' ')} #{actual_formatted}") - end - end - end - end -end diff --git a/spec/rspec/expectations/expectation_target_spec.rb b/spec/rspec/expectations/expectation_target_spec.rb index 26e432896..e4c2b6755 100644 --- a/spec/rspec/expectations/expectation_target_spec.rb +++ b/spec/rspec/expectations/expectation_target_spec.rb @@ -68,13 +68,13 @@ module Expectations it 'does not support operator matchers from #to' do expect { expect(3).to == 3 - }.to raise_error(ArgumentError) + }.to raise_error(ArgumentError, /The expect syntax does not support operator matchers, so you must pass a matcher to `#to`/) end it 'does not support operator matchers from #not_to' do expect { expect(3).not_to == 4 - }.to raise_error(ArgumentError) + }.to raise_error(ArgumentError, /The expect syntax does not support operator matchers, so you must pass a matcher to `#not_to`/) end end diff --git a/spec/rspec/matchers/built_in/be_spec.rb b/spec/rspec/matchers/built_in/be_spec.rb index fd98aff07..d80a8eef8 100644 --- a/spec/rspec/matchers/built_in/be_spec.rb +++ b/spec/rspec/matchers/built_in/be_spec.rb @@ -746,20 +746,6 @@ def send end end -RSpec.describe "should be =~" do - subject { "a string" } - - it "passes when =~ operator returns true" do - should be =~ /str/ - end - - it "fails when =~ operator returns false" do - expect { - should be =~ /blah/ - }.to fail_with(%(expected: =~ /blah/\n got: "a string")) - end -end - RSpec.describe "expect(...).to be ===" do it "passes when === operator returns true" do expect(Hash).to be === {} diff --git a/spec/rspec/matchers/built_in/operators_spec.rb b/spec/rspec/matchers/built_in/operators_spec.rb deleted file mode 100644 index a24ab290b..000000000 --- a/spec/rspec/matchers/built_in/operators_spec.rb +++ /dev/null @@ -1,251 +0,0 @@ -class MethodOverrideObject - def method - :foo - end -end - -class MethodMissingObject < Struct.new(:original) - undef == - - def method_missing(name, *args, &block) - original.__send__ name, *args, &block - end -end - -RSpec.describe "operator matchers" do - describe "should ==" do - subject { 'apple'.dup } - - it "delegates message to target" do - expect(subject).to receive(:==).with("apple").and_return(true) - should == "apple" - end - - it "returns true on success" do - expect(should == "apple").to be(true) - end - - it "fails when target.==(actual) returns false" do - expect(RSpec::Expectations).to receive(:fail_with).with(%{expected: "orange"\n got: "apple" (using ==)}, "orange", "apple") - should == "orange" - end - - context "when #method is overriden" do - subject(:myobj) { MethodOverrideObject.new } - it { expect { should == myobj }.to_not raise_error } - end - - context "when implemented via method_missing" do - let(:obj) { Object.new } - subject(:myobj) { MethodMissingObject.new(obj) } - - it { should == obj } - it { should_not == Object.new } - end - end - - describe "unsupported operators" do - subject { "apple" } - - it "raises an appropriate error for should != expected" do - expect { - should != "pear" - }.to raise_error(/does not support `should != expected`. Use `should_not == expected`/) - end - - it "raises an appropriate error for should_not != expected" do - expect { - should_not != "pear" - }.to raise_error(/does not support `should_not != expected`. Use `should == expected`/) - end - - it "raises an appropriate error for should !~ expected" do - expect { - should !~ /regex/ - }.to raise_error(/does not support `should !~ expected`. Use `should_not =~ expected`/) - end - - it "raises an appropriate error for should_not !~ expected" do - expect { - should_not !~ /regex/ - }.to raise_error(/does not support `should_not !~ expected`. Use `should =~ expected`/) - end - end - - describe "should_not ==" do - subject { "orange" } - - it "delegates message to target" do - expect(subject).to receive(:==).with("apple").and_return(false) - should_not == "apple" - end - - it "returns false on success" do - expect(should_not == "apple").to be(false) - end - - it "fails when target.==(actual) returns true" do - expect(RSpec::Expectations).to receive(:fail_with).with(%(expected not: == "orange"\n got: "orange"), "orange", "orange") - should_not == "orange" - end - end - - describe "should ===" do - subject { "apple" } - - it "delegates message to target" do - expect(subject).to receive(:===).with("apple").and_return(true) - should === "apple" - end - - it "fails when target.===(actual) returns false" do - expect(subject).to receive(:===).with("orange").and_return(false) - expect(RSpec::Expectations).to receive(:fail_with).with(%{expected: "orange"\n got: "apple" (using ===)}, "orange", "apple") - should === "orange" - end - end - - describe "should_not ===" do - subject { "apple" } - - it "delegates message to target" do - expect(subject).to receive(:===).with("apple").and_return(false) - should_not === "apple" - end - - it "fails when target.===(actual) returns true" do - expect(subject).to receive(:===).with("apple").and_return(true) - expect(RSpec::Expectations).to receive(:fail_with).with(%(expected not: === "apple"\n got: "apple"), "apple", "apple") - should_not === "apple" - end - end - - describe "should =~" do - subject { "foo" } - - it "delegates message to target" do - expect(subject).to receive(:=~).with(/oo/).and_return(true) - should =~ /oo/ - end - - it "fails when target.=~(actual) returns false" do - expect(RSpec::Expectations).to receive(:fail_with).with(%{expected: /fu/\n got: "foo" (using =~)}, /fu/, "foo") - should =~ /fu/ - end - end - - describe "should_not =~" do - subject { "foo" } - - it "delegates message to target" do - expect(subject).to receive(:=~).with(/fu/).and_return(false) - should_not =~ /fu/ - end - - it "fails when target.=~(actual) returns false" do - expect(RSpec::Expectations).to receive(:fail_with).with(%(expected not: =~ /oo/\n got: "foo"), /oo/, "foo") - should_not =~ /oo/ - end - end - - describe "should >" do - subject { 4 } - - it "passes if > passes" do - should > 3 - end - - it "fails if > fails" do - expect(RSpec::Expectations).to receive(:fail_with).with("expected: > 5\n got: 4", 5, 4) - should > 5 - end - end - - describe "should >=" do - subject { 4 } - - it "passes if actual == expected" do - should >= 4 - end - - it "passes if actual > expected" do - should >= 3 - end - - it "fails if > fails" do - expect(RSpec::Expectations).to receive(:fail_with).with("expected: >= 5\n got: 4", 5, 4) - should >= 5 - end - end - - describe "should <" do - subject { 4 } - - it "passes if < passes" do - should < 5 - end - - it "fails if > fails" do - expect(RSpec::Expectations).to receive(:fail_with).with("expected: < 3\n got: 4", 3, 4) - should < 3 - end - end - - describe "should <=" do - subject { 4 } - - it "passes if actual == expected" do - should <= 4 - end - - it "passes if actual < expected" do - should <= 5 - end - - it "fails if > fails" do - expect(RSpec::Expectations).to receive(:fail_with).with("expected: <= 3\n got: 4", 3, 4) - should <= 3 - end - end - - describe "OperatorMatcher registry" do - let(:custom_klass) { Class.new } - let(:custom_subklass) { Class.new(custom_klass) } - - after { - RSpec::Matchers::BuiltIn::OperatorMatcher.unregister(custom_klass, "=~") - } - - it "allows operator matchers to be registered for classes" do - RSpec::Matchers::BuiltIn::OperatorMatcher.register(custom_klass, "=~", RSpec::Matchers::BuiltIn::Match) - expect(RSpec::Matchers::BuiltIn::OperatorMatcher.get(custom_klass, "=~")).to eq(RSpec::Matchers::BuiltIn::Match) - end - - it "considers ancestors when finding an operator matcher" do - RSpec::Matchers::BuiltIn::OperatorMatcher.register(custom_klass, "=~", RSpec::Matchers::BuiltIn::Match) - expect(RSpec::Matchers::BuiltIn::OperatorMatcher.get(custom_subklass, "=~")).to eq(RSpec::Matchers::BuiltIn::Match) - end - - it "returns nil if there is no matcher registered for a class" do - expect(RSpec::Matchers::BuiltIn::OperatorMatcher.get(custom_klass, "=~")).to be_nil - end - end - - describe RSpec::Matchers::BuiltIn::PositiveOperatorMatcher do - subject(:o) { Object.new } - - it "works when the target has implemented #send" do - def o.send(*_args); raise "DOH! Library developers shouldn't use #send!" end - expect { should == o }.not_to raise_error - end - end - - describe RSpec::Matchers::BuiltIn::NegativeOperatorMatcher do - subject(:o) { Object.new } - - it "works when the target has implemented #send" do - def o.send(*_args); raise "DOH! Library developers shouldn't use #send!" end - expect { should_not == :foo }.not_to raise_error - end - end -end