module Shoulda # :nodoc:
module Matchers
module Independent # :nodoc:
# Ensure that a given method is delegated properly.
#
# Basic Syntax:
# 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
#
# Examples:
# 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) }
#
def delegate_method(delegating_method)
DelegateMatcher.new(delegating_method)
end
class DelegateMatcher
def initialize(delegating_method)
@delegating_method = delegating_method
@delegated_arguments = []
end
def matches?(_subject)
@subject = _subject
ensure_target_method_is_present!
stub_target
begin
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}"
)
end
def does_not_match?(subject)
raise InvalidDelegateMatcher
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 #{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}"
end
if method_on_target.present?
message << " as ##{method_on_target}"
end
message
end
def delegating_method_name
method_name_with_class(delegating_method)
end
def target_method_name
method_name_with_class(target_method)
end
def method_name_with_class(method)
if Class === subject
subject.name + '.' + method.to_s
else
subject.class.name + '#' + method.to_s
end
end
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?
raise TargetNotDefinedError
end
end
end
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)`'.squish
end
end
class DelegateMatcher::InvalidDelegateMatcher < StandardError
def message
'#delegate_to does not support #should_not syntax.'
end
end
end
end
end