diff --git a/README.md b/README.md index efb60b8a..3d623a1a 100644 --- a/README.md +++ b/README.md @@ -1324,6 +1324,11 @@ Matchers to test non-Rails-dependent code: class Human < ActiveRecord::Base has_one :robot delegate :work, to: :robot + + # alternatively, if you are not using Rails + def work + robot.work + end end # RSpec diff --git a/lib/shoulda/matchers/independent.rb b/lib/shoulda/matchers/independent.rb index e8119a10..4a72f929 100644 --- a/lib/shoulda/matchers/independent.rb +++ b/lib/shoulda/matchers/independent.rb @@ -1,8 +1,9 @@ require 'shoulda/matchers/independent/delegate_matcher' +require 'shoulda/matchers/independent/delegate_matcher/stubbed_target' module Shoulda module Matchers - # = Matchers for non-Rails-dependent code. + # Matchers for non-Rails-dependent code. module Independent end end diff --git a/lib/shoulda/matchers/independent/delegate_matcher.rb b/lib/shoulda/matchers/independent/delegate_matcher.rb index bcf0caa5..8532c545 100644 --- a/lib/shoulda/matchers/independent/delegate_matcher.rb +++ b/lib/shoulda/matchers/independent/delegate_matcher.rb @@ -1,5 +1,3 @@ -require 'active_support/deprecation' - module Shoulda # :nodoc: module Matchers module Independent # :nodoc: @@ -10,13 +8,14 @@ module Shoulda # :nodoc: # it { should delegate_method(:deliver_mail).to(:mailman) } # # Options: - # * :as - tests that the object being delegated to is called with a certain method - # (defaults to same name as delegating method) - # * :with_arguments - tests that the method on the object being delegated to is - # called with certain arguments + # * :as - tests that the object being delegated to is called + # with a certain method (defaults to same name as delegating method) + # * :with_arguments - tests that the method on the object being + # delegated to is called with certain arguments # # Examples: - # it { should delegate_method(:deliver_mail).to(:mailman).as(:deliver_with_haste) + # it { should delegate_method(:deliver_mail).to(:mailman). + # as(:deliver_with_haste) } # it { should delegate_method(:deliver_mail).to(:mailman). # with_arguments('221B Baker St.', :hastily => true) } # @@ -27,28 +26,26 @@ module Shoulda # :nodoc: class DelegateMatcher def initialize(delegating_method) @delegating_method = delegating_method + @delegated_arguments = [] end - def matches?(subject) - @subject = subject + def matches?(_subject) + @subject = _subject ensure_target_method_is_present! + stub_target begin - extend Mocha::API - - stubbed_object = stub(method_on_target) - subject.stubs(@target_method).returns(stubbed_object) - subject.send(@delegating_method) - - matcher = Mocha::API::HaveReceived.new(method_on_target).with(*@delegated_arguments) - matcher.matches?(stubbed_object) - rescue NoMethodError, MiniTest::Assertion + subject.send(delegating_method, *delegated_arguments) + target_has_received_delegated_method? && target_has_received_arguments? + rescue NoMethodError false end end def description - add_clarifications_to("delegate method ##{@delegating_method} to :#{@target_method}") + add_clarifications_to( + "delegate method ##{delegating_method} to :#{target_method}" + ) end def does_not_match?(subject) @@ -70,47 +67,74 @@ module Shoulda # :nodoc: self end - def failure_message_for_should + def failure_message base = "Expected #{delegating_method_name} to delegate to #{target_method_name}" add_clarifications_to(base) end + alias failure_message_for_should failure_message private + attr_reader :delegated_arguments, :delegating_method, :method, :subject, + :target_method, :method_on_target + def add_clarifications_to(message) - if @delegated_arguments.present? - message << " with arguments: #{@delegated_arguments.inspect}" + if delegated_arguments.present? + message << " with arguments: #{delegated_arguments.inspect}" end - if @method_on_target.present? - message << " as ##{@method_on_target}" + if method_on_target.present? + message << " as ##{method_on_target}" end message end def delegating_method_name - method_name_with_class(@delegating_method) + method_name_with_class(delegating_method) end def target_method_name - method_name_with_class(@target_method) + method_name_with_class(target_method) end def method_name_with_class(method) - if Class === @subject - @subject.name + '.' + method.to_s + if Class === subject + subject.name + '.' + method.to_s else - @subject.class.name + '#' + method.to_s + subject.class.name + '#' + method.to_s end end - def method_on_target - @method_on_target || @delegating_method + def target_has_received_delegated_method? + stubbed_target.has_received_method? + end + + def target_has_received_arguments? + stubbed_target.has_received_arguments?(*delegated_arguments) + end + + def stubbed_method + method_on_target || delegating_method + end + + def stub_target + local_stubbed_target = stubbed_target + local_target_method = target_method + + subject.instance_eval do + define_singleton_method local_target_method do + local_stubbed_target + end + end + end + + def stubbed_target + @stubbed_target ||= StubbedTarget.new(stubbed_method) end def ensure_target_method_is_present! - if @target_method.blank? + if target_method.blank? raise TargetNotDefinedError end end @@ -118,7 +142,8 @@ module Shoulda # :nodoc: class DelegateMatcher::TargetNotDefinedError < StandardError def message - 'Delegation needs a target. Use the #to method to define one, e.g. `post_office.should delegate(:deliver_mail).to(:mailman)`' + 'Delegation needs a target. Use the #to method to define one, e.g. + `post_office.should delegate(:deliver_mail).to(:mailman)`'.squish end end diff --git a/lib/shoulda/matchers/independent/delegate_matcher/stubbed_target.rb b/lib/shoulda/matchers/independent/delegate_matcher/stubbed_target.rb new file mode 100644 index 00000000..cafbe740 --- /dev/null +++ b/lib/shoulda/matchers/independent/delegate_matcher/stubbed_target.rb @@ -0,0 +1,34 @@ +module Shoulda # :nodoc: + module Matchers + module Independent # :nodoc: + class DelegateMatcher::StubbedTarget # :nodoc: + def initialize(method) + @received_method = false + @received_arguments = [] + stub_method(method) + end + + def has_received_method? + received_method + end + + def has_received_arguments?(*args) + args == received_arguments + end + + private + + def stub_method(method) + class_eval do + define_method method do |*args| + @received_method = true + @received_arguments = args + end + end + end + + attr_reader :received_method, :received_arguments + end + end + end +end diff --git a/spec/shoulda/matchers/independent/delegate_matcher/stubbed_target_spec.rb b/spec/shoulda/matchers/independent/delegate_matcher/stubbed_target_spec.rb new file mode 100644 index 00000000..b0d02e07 --- /dev/null +++ b/spec/shoulda/matchers/independent/delegate_matcher/stubbed_target_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Shoulda::Matchers::Independent::DelegateMatcher::StubbedTarget do + subject(:target) { described_class.new(:stubbed_method) } + + describe '#has_received_method?' do + it 'returns true when the method has been called on the target' do + target.stubbed_method + + expect(target).to have_received_method + end + + it 'returns false when the method has not been called on the target' do + expect(target).not_to have_received_method + end + end + + describe '#has_received_arguments?' do + context 'method is called with specified arguments' do + it 'returns true' do + target.stubbed_method(:arg1, :arg2) + + expect(target).to have_received_arguments(:arg1, :arg2) + end + end + + context 'method is not called with specified arguments' do + it 'returns false' do + target.stubbed_method + + expect(target).not_to have_received_arguments(:arg1) + end + end + + context 'method is called with arguments in incorrect order' do + it 'returns false' do + target.stubbed_method(:arg2, :arg1) + + expect(target).not_to have_received_arguments(:arg1, :arg2) + end + end + end +end diff --git a/spec/shoulda/matchers/independent/delegate_matcher_spec.rb b/spec/shoulda/matchers/independent/delegate_matcher_spec.rb index 47fb427c..bc31b124 100644 --- a/spec/shoulda/matchers/independent/delegate_matcher_spec.rb +++ b/spec/shoulda/matchers/independent/delegate_matcher_spec.rb @@ -1,59 +1,46 @@ require 'spec_helper' describe Shoulda::Matchers::Independent::DelegateMatcher do - context '#description' do + describe '#description' do context 'by default' do it 'states that it should delegate method to the right object' do matcher = delegate_method(:method_name).to(:target) - matcher.description.should == 'delegate method #method_name to :target' + expect(matcher.description) + .to eq 'delegate method #method_name to :target' end end context 'with #as chain' do it 'states that it should delegate method to the right object and method' do matcher = delegate_method(:method_name).to(:target).as(:alternate) + message = 'delegate method #method_name to :target as #alternate' - matcher.description.should == 'delegate method #method_name to :target as #alternate' + expect(matcher.description).to eq message end end context 'with #with_argument chain' do it 'states that it should delegate method to the right object with right argument' do - matcher = delegate_method(:method_name).to(:target).with_arguments(:foo, :bar => [1, 2]) + matcher = delegate_method(:method_name).to(:target) + .with_arguments(:foo, bar: [1, 2]) + message = 'delegate method #method_name to :target with arguments: [:foo, {:bar=>[1, 2]}]' - matcher.description.should == - 'delegate method #method_name to :target with arguments: [:foo, {:bar=>[1, 2]}]' + expect(matcher.description).to eq message end end end - it 'supports chaining on #to' do - matcher = delegate_method(:method) - - matcher.to(:another_method).should == matcher - end - - it 'supports chaining on #with_arguments' do - matcher = delegate_method(:method) - - matcher.with_arguments(1, 2, 3).should == matcher - end - - it 'supports chaining on #as' do - matcher = delegate_method(:method) - - matcher.as(:some_other_method).should == matcher - end - it 'raises an error if no delegation target is defined' do - expect { Object.new.should delegate_method(:name) }. - to raise_exception described_class::TargetNotDefinedError + expect { + delegate_method(:name).matches?(Object.new) + }.to raise_exception described_class::TargetNotDefinedError end it 'raises an error if called with #should_not' do - expect { Object.new.should_not delegate_method(:name).to(:anyone) }. - to raise_exception described_class::InvalidDelegateMatcher + expect { + delegate_method(:name).to(:anyone).does_not_match?(Object.new) + }.to raise_exception described_class::InvalidDelegateMatcher end context 'given a method that does not delegate' do @@ -69,32 +56,31 @@ describe Shoulda::Matchers::Independent::DelegateMatcher do post_office = PostOffice.new matcher = delegate_method(:deliver_mail).to(:mailman) - matcher.matches?(post_office).should be_false + expect(matcher.matches?(post_office)).to be false end it 'has a failure message that indicates which method should have been delegated' do post_office = PostOffice.new - matcher = delegate_method(:deliver_mail).to(:mailman) - - matcher.matches?(post_office) - message = 'Expected PostOffice#deliver_mail to delegate to PostOffice#mailman' - matcher.failure_message_for_should.should == message + + expect { + expect(post_office).to delegate_method(:deliver_mail).to(:mailman) + }.to fail_with_message(message) end it 'uses the proper syntax for class methods in errors' do - matcher = delegate_method(:deliver_mail).to(:mailman) - - matcher.matches?(PostOffice) - message = 'Expected PostOffice.deliver_mail to delegate to PostOffice.mailman' - matcher.failure_message_for_should.should == message + + expect { + expect(PostOffice).to delegate_method(:deliver_mail).to(:mailman) + }.to fail_with_message(message) end end context 'given a method that delegates properly' do it 'accepts' do define_class(:mailman) + define_class(:post_office) do def deliver_mail mailman.deliver_mail @@ -103,16 +89,23 @@ describe Shoulda::Matchers::Independent::DelegateMatcher do def mailman Mailman.new end - end.new.should delegate_method(:deliver_mail).to(:mailman) + end + + post_office = PostOffice.new + + expect(post_office).to delegate_method(:deliver_mail).to(:mailman) end end - context 'given a method that delegates properly with certain arguments' do + context 'given a method that delegates properly with arguments' do + let(:post_office) { PostOffice.new } + before do define_class(:mailman) + define_class(:post_office) do - def deliver_mail - mailman.deliver_mail('221B Baker St.', :hastily => true) + def deliver_mail(*args) + mailman.deliver_mail('221B Baker St.', hastily: true) end def mailman @@ -123,34 +116,36 @@ describe Shoulda::Matchers::Independent::DelegateMatcher do context 'when given the correct arguments' do it 'accepts' do - PostOffice.new.should delegate_method(:deliver_mail). - to(:mailman).with_arguments('221B Baker St.', :hastily => true) + expect(post_office).to delegate_method(:deliver_mail) + .to(:mailman).with_arguments('221B Baker St.', hastily: true) end end context 'when not given the correct arguments' do it 'rejects' do - post_office = PostOffice.new - matcher = delegate_method(:deliver_mail).to(:mailman). - with_arguments('123 Nowhere Ln.') - matcher.matches?(post_office).should be_false + matcher = delegate_method(:deliver_mail).to(:mailman) + .with_arguments('123 Nowhere Ln.') + + expect(matcher.matches?(post_office)).to be_false end it 'has a failure message that indicates which arguments were expected' do - post_office = PostOffice.new - matcher = delegate_method(:deliver_mail).to(:mailman).with_arguments('123 Nowhere Ln.') - - matcher.matches?(post_office) - message = 'Expected PostOffice#deliver_mail to delegate to PostOffice#mailman with arguments: ["123 Nowhere Ln."]' - matcher.failure_message_for_should.should == message + + expect { + expect(post_office).to delegate_method(:deliver_mail) + .to(:mailman).with_arguments('123 Nowhere Ln.') + }.to fail_with_message(message) end end end context 'given a method that delegates properly to a method of a different name' do + let(:post_office) { PostOffice.new } + before do define_class(:mailman) + define_class(:post_office) do def deliver_mail mailman.deliver_mail_and_avoid_dogs @@ -164,41 +159,26 @@ describe Shoulda::Matchers::Independent::DelegateMatcher do context 'when given the correct method name' do it 'accepts' do - PostOffice.new. - should delegate_method(:deliver_mail).to(:mailman).as(:deliver_mail_and_avoid_dogs) + expect(post_office).to delegate_method(:deliver_mail) + .to(:mailman).as(:deliver_mail_and_avoid_dogs) end end context 'when given an incorrect method name' do it 'rejects' do - post_office = PostOffice.new matcher = delegate_method(:deliver_mail).to(:mailman).as(:watch_tv) - matcher.matches?(post_office).should be_false + + expect(matcher.matches?(post_office)).to be_false end it 'has a failure message that indicates which method was expected' do - post_office = PostOffice.new - matcher = delegate_method(:deliver_mail).to(:mailman).as(:watch_tv) - - matcher.matches?(post_office) - message = 'Expected PostOffice#deliver_mail to delegate to PostOffice#mailman as #watch_tv' - matcher.failure_message_for_should.should == message + + expect { + expect(post_office).to delegate_method(:deliver_mail) + .to(:mailman).as(:watch_tv) + }.to fail_with_message(message) end end end end - -describe Shoulda::Matchers::Independent::DelegateMatcher::TargetNotDefinedError do - it 'has a useful message' do - error = Shoulda::Matchers::Independent::DelegateMatcher::TargetNotDefinedError.new - error.message.should include 'Delegation needs a target' - end -end - -describe Shoulda::Matchers::Independent::DelegateMatcher::InvalidDelegateMatcher do - it 'has a useful message' do - error = Shoulda::Matchers::Independent::DelegateMatcher::InvalidDelegateMatcher.new - error.message.should include 'does not support #should_not' - end -end