module Spec
module Mocks
class Proxy
DEFAULT_OPTIONS = {
:null_object => false,
}
@@warn_about_expectations_on_nil = true
def self.allow_message_expectations_on_nil
@@warn_about_expectations_on_nil = false
# ensure nil.rspec_verify is called even if an expectation is not set in the example
# otherwise the allowance would effect subsequent examples
$rspec_mocks.add(nil) unless $rspec_mocks.nil?
end
def initialize(target, name=nil, options={})
@target = target
@name = name
@error_generator = ErrorGenerator.new target, name, options
@expectation_ordering = OrderGroup.new @error_generator
@expectations = []
@messages_received = []
@stubs = []
@proxied_methods = []
@options = options ? DEFAULT_OPTIONS.dup.merge(options) : DEFAULT_OPTIONS
@already_proxied_respond_to = false
end
def null_object?
@options[:null_object]
end
def as_null_object
@options[:null_object] = true
@target
end
def add_message_expectation(expected_from, sym, opts={}, &block)
__add sym
warn_if_nil_class sym
if existing_stub = @stubs.detect {|s| s.sym == sym }
expectation = existing_stub.build_child(expected_from, block_given?? block : nil, 1, opts)
else
expectation = MessageExpectation.new(@error_generator, @expectation_ordering, expected_from, sym, block_given? ? block : nil, 1, opts)
end
@expectations << expectation
@expectations.last
end
def add_negative_message_expectation(expected_from, sym, &block)
__add sym
warn_if_nil_class sym
@expectations << NegativeMessageExpectation.new(@error_generator, @expectation_ordering, expected_from, sym, block_given? ? block : nil)
@expectations.last
end
def add_stub(expected_from, sym, opts={}, &implementation)
__add sym
@stubs.unshift MessageExpectation.new(@error_generator, @expectation_ordering, expected_from, sym, nil, :any, opts, &implementation)
@stubs.first
end
def remove_stub(message)
message = message.to_sym
if stub_to_remove = @stubs.detect { |s| s.matches_name?(message) }
reset_proxied_method(message)
@stubs.delete(stub_to_remove)
else
raise MockExpectationError, "The method `#{message}` was not stubbed or was already unstubbed"
end
end
def verify #:nodoc:
verify_expectations
ensure
reset
end
def reset
clear_expectations
clear_stubs
reset_proxied_methods
clear_proxied_methods
reset_nil_expectations_warning
end
def received_message?(sym, *args, &block)
@messages_received.any? {|array| array == [sym, args, block]}
end
def has_negative_expectation?(sym)
@expectations.detect {|expectation| expectation.negative_expectation_for?(sym)}
end
def record_message_received(sym, args, block)
@messages_received << [sym, args, block]
end
def message_received(sym, *args, &block)
expectation = find_matching_expectation(sym, *args)
stub = find_matching_method_stub(sym, *args)
if (stub && expectation && expectation.called_max_times?) || (stub && !expectation)
if expectation = find_almost_matching_expectation(sym, *args)
expectation.advise(*args) unless expectation.expected_messages_received?
end
stub.invoke(*args, &block)
elsif expectation
expectation.invoke(*args, &block)
elsif expectation = find_almost_matching_expectation(sym, *args)
expectation.advise(*args) if null_object? unless expectation.expected_messages_received?
raise_unexpected_message_args_error(expectation, *args) unless (has_negative_expectation?(sym) or null_object?)
elsif stub = find_almost_matching_stub(sym, *args)
stub.advise(*args)
raise_unexpected_message_args_error(stub, *args)
elsif @target.is_a?(Class) && @target.superclass.respond_to?(sym, true)
@target.superclass.send(sym, *args, &block)
else
@target.__send__ :method_missing, sym, *args, &block
end
end
def raise_unexpected_message_args_error(expectation, *args)
@error_generator.raise_unexpected_message_args_error expectation, *args
end
def raise_unexpected_message_error(sym, *args)
@error_generator.raise_unexpected_message_error sym, *args
end
def find_matching_method_stub(sym, *args)
@stubs.find {|stub| stub.matches(sym, args)}
end
private
def __add(sym)
$rspec_mocks.add(@target) unless $rspec_mocks.nil?
define_expected_method(sym)
end
def warn_if_nil_class(sym)
if proxy_for_nil_class? & @@warn_about_expectations_on_nil
Kernel.warn("An expectation of :#{sym} was set on nil. Called from #{caller[2]}. Use allow_message_expectations_on_nil to disable warnings.")
end
end
def define_expected_method(sym)
unless @proxied_methods.include?(sym)
visibility_string = "#{visibility(sym)} :#{sym}"
if target_responds_to?(sym)
munged_sym = munge(sym)
target_metaclass.instance_eval do
alias_method munged_sym, sym if method_defined?(sym)
end
@proxied_methods << sym
end
target_metaclass.class_eval(<<-EOF, __FILE__, __LINE__)
def #{sym}(*args, &block)
__mock_proxy.message_received :#{sym}, *args, &block
end
#{visibility_string}
EOF
end
end
def target_responds_to?(sym)
return @target.__send__(munge(:respond_to?),sym) if @already_proxied_respond_to
return @already_proxied_respond_to = true if sym == :respond_to?
return @target.respond_to?(sym, true)
end
def visibility(sym)
if Mock === @target
'public'
elsif target_metaclass.private_method_defined?(sym)
'private'
elsif target_metaclass.protected_method_defined?(sym)
'protected'
else
'public'
end
end
def munge(sym)
"proxied_by_rspec__#{sym}"
end
def clear_expectations
@expectations.clear
end
def clear_stubs
@stubs.clear
end
def clear_proxied_methods
@proxied_methods.clear
end
def target_metaclass
class << @target; self; end
end
def verify_expectations
@expectations.each do |expectation|
expectation.verify_messages_received
end
end
def reset_proxied_methods
@proxied_methods.each do |sym|
reset_proxied_method(sym)
end
end
def reset_proxied_method(sym)
munged_sym = munge(sym)
target_metaclass.instance_eval do
remove_method sym
if method_defined?(munged_sym)
alias_method sym, munged_sym
remove_method munged_sym
end
end
end
def proxy_for_nil_class?
@target.nil?
end
def reset_nil_expectations_warning
@@warn_about_expectations_on_nil = true if proxy_for_nil_class?
end
def find_matching_expectation(sym, *args)
@expectations.find {|expectation| expectation.matches(sym, args) && !expectation.called_max_times?} ||
@expectations.find {|expectation| expectation.matches(sym, args)}
end
def find_almost_matching_expectation(sym, *args)
@expectations.find {|expectation| expectation.matches_name_but_not_args(sym, args)}
end
def find_almost_matching_stub(sym, *args)
@stubs.find {|stub| stub.matches_name_but_not_args(sym, args)}
end
end
end
end