1
0
Fork 0
mirror of https://github.com/thoughtbot/shoulda-matchers.git synced 2022-11-09 12:01:38 -05:00
thoughtbot--shoulda-matchers/lib/shoulda/matchers/independent/delegate_matcher.rb
2014-06-27 14:41:54 -06:00

296 lines
8.3 KiB
Ruby

module Shoulda
module Matchers
module Independent
# The `delegate_method` matcher tests that an object forwards messages
# to other, internal objects by way of delegation.
#
# In this example, we test that Courier forwards a call to #deliver onto
# its PostOffice instance:
#
# require 'forwardable'
#
# class Courier
# extend Forwardable
#
# attr_reader :post_office
#
# def_delegators :post_office, :deliver
#
# def initialize
# @post_office = PostOffice.new
# end
# end
#
# # RSpec
# describe Courier do
# it { should delegate_method(:deliver).to(:post_office) }
# end
#
# # Test::Unit
# class CourierTest < Test::Unit::TestCase
# should delegate_method(:deliver).to(:post_office)
# end
#
# To employ some terminology, we would say that Courier's #deliver method
# is the delegating method, PostOffice is the delegate object, and
# PostOffice#deliver is the delegate method.
#
# #### Qualifiers
#
# ##### as
#
# Use `as` if the name of the delegate method is different from the name
# of the delegating method.
#
# Here, Courier has a #deliver method, but instead of calling #deliver on
# the PostOffice, it calls #ship:
#
# class Courier
# attr_reader :post_office
#
# def initialize
# @post_office = PostOffice.new
# end
#
# def deliver(package)
# post_office.ship(package)
# end
# end
#
# # RSpec
# describe Courier do
# it { should delegate_method(:deliver).to(:post_office).as(:ship) }
# end
#
# # Test::Unit
# class CourierTest < Test::Unit::TestCase
# should delegate_method(:deliver).to(:post_office).as(:ship)
# end
#
# ##### with_arguments
#
# Use `with_arguments` to assert that the delegate method is called with
# certain arguments.
#
# Here, when Courier#deliver calls PostOffice#ship, it adds an options
# hash:
#
# class Courier
# attr_reader :post_office
#
# def initialize
# @post_office = PostOffice.new
# end
#
# def deliver(package)
# post_office.ship(package, expedited: true)
# end
# end
#
# # RSpec
# describe Courier do
# it do
# should delegate_method(:deliver).
# to(:post_office).
# as(:ship).
# with_arguments(expedited: true)
# end
# end
#
# # Test::Unit
# class CourierTest < Test::Unit::TestCase
# should delegate_method(:deliver).
# to(:post_office).
# as(:ship).
# with_arguments(expedited: true)
# end
#
# @return [DelegateMatcher]
#
def delegate_method(delegating_method)
DelegateMatcher.new(delegating_method)
end
# @private
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
protected
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.double_collection_for(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
# @private
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