lib/shoulda/matchers/independent/delegate_matcher.rb
module Shoulda # :nodoc: module Matchers module Independent # :nodoc: # Ensure that a given method is delegated properly. # # Basic Syntax: # it { should delegate_method(method_name).to(delegate_name) } # # Options: # * <tt>:as</tt> - The name of the delegating method. Defaults to # method_name. # * <tt>:with_arguments</tt> - Tests that the delegate method is called # with certain arguments. # # Examples: # it { should delegate_method(:deliver_mail).to(:mailman) } # it { should delegate_method(:deliver_mail).to(:mailman). # as(:deliver_mail_via_mailman) } # it { should delegate_method(:deliver_mail).to(:mailman). # as(:deliver_mail_hastily). # with_arguments('221B Baker St.', hastily: true) } # def delegate_method(delegating_method) DelegateMatcher.new(delegating_method) end class DelegateMatcher def initialize(delegating_method) @delegating_method = delegating_method @method_on_target = @delegating_method @target_double = Doublespeak::ObjectDouble.new @delegated_arguments = [] @target_method = nil @subject = nil @subject_double_collection = nil end def matches?(subject) @subject = subject ensure_target_method_is_present! subject_has_delegating_method? && subject_has_target_method? && subject_delegates_to_target_correctly? end def description add_clarifications_to( "delegate method ##{delegating_method} to :#{target_method}" ) end def to(target_method) @target_method = target_method self end def as(method_on_target) @method_on_target = method_on_target self end def with_arguments(*arguments) @delegated_arguments = arguments self end def failure_message base = "Expected #{formatted_delegating_method_name} to delegate to #{formatted_target_method_name}" add_clarifications_to(base) base << "\nCalls on #{formatted_target_method_name}:" base << formatted_calls_on_target base.strip end alias failure_message_for_should failure_message def failure_message_when_negated base = "Expected #{formatted_delegating_method_name} not to delegate to #{formatted_target_method_name}" add_clarifications_to(base) base << ', but it did' end alias failure_message_for_should_not failure_message_when_negated private attr_reader \ :delegated_arguments, :delegating_method, :method, :method_on_target, :subject, :subject_double_collection, :target_double, :target_method def add_clarifications_to(message) if delegated_arguments.any? message << " with arguments: #{delegated_arguments.inspect}" end if method_on_target != delegating_method message << " as ##{method_on_target}" end message end def formatted_delegating_method_name formatted_method_name_for(delegating_method) end def formatted_target_method_name formatted_method_name_for(target_method) end def formatted_method_name_for(method_name) if subject.is_a?(Class) subject.name + '.' + method_name.to_s else subject.class.name + '#' + method_name.to_s end end def target_received_method? calls_to_method_on_target.any? end def target_received_method_with_delegated_arguments? calls_to_method_on_target.any? do |call| call.args == delegated_arguments end end def subject_has_delegating_method? subject.respond_to?(delegating_method) end def subject_has_target_method? subject.respond_to?(target_method) end def ensure_target_method_is_present! if target_method.blank? raise TargetNotDefinedError end end def subject_delegates_to_target_correctly? register_subject_double_collection Doublespeak.with_doubles_activated do subject.public_send(delegating_method, *delegated_arguments) end if delegated_arguments.any? target_received_method_with_delegated_arguments? else target_received_method? end end def register_subject_double_collection double_collection = Doublespeak.register_double_collection(subject.singleton_class) double_collection.register_stub(target_method). to_return(target_double) @subject_double_collection = double_collection end def calls_to_method_on_target target_double.calls_to(method_on_target) end def calls_on_target target_double.calls end def formatted_calls_on_target string = "" if calls_on_target.any? string << "\n" calls_on_target.each_with_index do |call, i| name = call.method_name args = call.args.map { |arg| arg.inspect }.join(', ') string << "#{i+1}) #{name}(#{args})\n" end else string << " (none)" end string end class 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)`'.squish end end end end end end