Skip to content

Commit

Permalink
Merge 486405f into a2f952a
Browse files Browse the repository at this point in the history
  • Loading branch information
xaviershay committed Oct 25, 2013
2 parents a2f952a + 486405f commit 0357303
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 23 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Expand Up @@ -31,6 +31,8 @@ Enhancements:
(Xavier Shay).
* Provide `object_double` to create verified doubles of specific object
instances (Xavier Shay).
* Provide 'verify_partial_mocks` configuration that provides `object_double`
like verification behaviour on partial mocks. (Xavier Shay)
* Improved performance of double creation, particularly those with many
attributes. (Xavier Shay)
* Default value of `transfer_nested_constants` option for constant stubbing can
Expand Down
12 changes: 12 additions & 0 deletions lib/rspec/mocks/configuration.rb
Expand Up @@ -7,6 +7,7 @@ def initialize
@yield_receiver_to_any_instance_implementation_blocks = true
@verify_doubled_constant_names = false
@transfer_nested_constants = false
@verify_partial_mocks = false
end

def yield_receiver_to_any_instance_implementation_blocks?
Expand Down Expand Up @@ -82,6 +83,17 @@ def transfer_nested_constants=(val)
@transfer_nested_constants = val
end

# When set to true, partial mocks will be verified the same as object
# doubles. Any stubs will have their arity checked against the original
# method, and methods that do not exist cannot be stubbed.
def verify_partial_mocks=(val)
@verify_partial_mocks = val
end

def verify_partial_mocks?
!!@verify_partial_mocks
end

# @api private
# Resets the configured syntax to the default.
def reset_syntaxes_to_default
Expand Down
6 changes: 5 additions & 1 deletion lib/rspec/mocks/space.rb
Expand Up @@ -57,7 +57,11 @@ def proxy_for(object)
when NilClass then ProxyForNil.new(expectation_ordering)
when TestDouble then object.__build_mock_proxy(expectation_ordering)
else
PartialMockProxy.new(object, expectation_ordering)
if RSpec::Mocks.configuration.verify_partial_mocks?
VerifyingPartialMockProxy.new(object, expectation_ordering)
else
PartialMockProxy.new(object, expectation_ordering)
end
end
end
end
Expand Down
94 changes: 72 additions & 22 deletions lib/rspec/mocks/verifying_proxy.rb
Expand Up @@ -4,6 +4,34 @@
module RSpec
module Mocks

module VerifyingProxyMethods
def add_stub(location, method_name, opts={}, &implementation)
ensure_implemented(method_name)
super
end

def add_simple_stub(method_name, *args)
ensure_implemented(method_name)
super
end

def add_message_expectation(location, method_name, opts={}, &block)
ensure_implemented(method_name)
super
end

def ensure_implemented(method_name)
return unless @doubled_module.defined?

method_reference[method_name].when_unimplemented do
@error_generator.raise_unimplemented_error(
@doubled_module,
method_name
)
end
end
end

# A verifying proxy mostly acts like a normal proxy, except that it
# contains extra logic to try and determine the validity of any expectation
# set on it. This includes whether or not methods have been defined and the
Expand All @@ -19,28 +47,15 @@ module Mocks
#
# @api private
class VerifyingProxy < Proxy
include VerifyingProxyMethods

def initialize(object, order_group, name, method_reference_class)
super(object, order_group)
@object = object
@doubled_module = name
@method_reference_class = method_reference_class
end

def add_stub(location, method_name, opts={}, &implementation)
ensure_implemented(method_name)
super
end

def add_simple_stub(method_name, *args)
ensure_implemented(method_name)
super
end

def add_message_expectation(location, method_name, opts={}, &block)
ensure_implemented(method_name)
super
end

# A custom method double is required to pass through a way to lookup
# methods to determine their arity. This is only relevant if the doubled
# class is loaded.
Expand All @@ -55,17 +70,27 @@ def method_reference
h[k] = @method_reference_class.new(@doubled_module, k)
end
end
end

def ensure_implemented(method_name)
return unless @doubled_module.defined?
class VerifyingPartialMockProxy < PartialMockProxy
include VerifyingProxyMethods

method_reference[method_name].when_unimplemented do
@error_generator.raise_unimplemented_error(
@doubled_module,
method_name
)
def initialize(object, expectation_ordering)
super(object, expectation_ordering)
@object = object
@doubled_module = DirectObjectReference.new(object)
@method_checker = :public_methods
end

# A custom method double is required to pass through a way to lookup
# methods to determine their arity.
def method_double
@method_double ||= Hash.new do |h,k|
h[k] = VerifyingExistingMethodDouble.new(@object, k, self)
end
end

alias_method :method_reference, :method_double
end

# @api private
Expand Down Expand Up @@ -99,5 +124,30 @@ def ensure_arity!(arity)
end
end
end

# @api private
#
# A VerifyingMethodDouble fetches the method to verify against from the
# original object, using a MethodReference. This works for pure doubles,
# but when the original object is itself the one being modified we need to
# collapse the reference and the method double into a single object so that
# we can access the original pristine method definition.
class VerifyingExistingMethodDouble < VerifyingMethodDouble
def initialize(object, method_name, proxy)
super(object, method_name, proxy, self)

@aliased_method = if object.__send__(:method_defined?, method_name)
object.__send__(:method, method_name)
end
end

def when_defined
yield @aliased_method if @aliased_method
end

def when_unimplemented
yield unless @aliased_method
end
end
end
end
29 changes: 29 additions & 0 deletions spec/rspec/mocks/partial_mock_spec.rb
Expand Up @@ -230,5 +230,34 @@ def private_method; end
end

end

describe 'when verify_partial_mocks configuration option is set' do
include_context "with isolated configuration"

# TODO: DRY
def prevents(&block)
expect(&block).to \
raise_error(RSpec::Mocks::MockExpectationError)
end

before do
RSpec::Mocks.configuration.verify_partial_mocks = true
end

it 'does not allow a non-existing method to be stubbed' do
prevents { expect(Object).to receive(:unimplemented) }
end

it 'verifies arity range when matching arguments' do
prevents { expect(Object).to receive(:to_s).with('bogus') }
end

it 'verifies arity range when receiving a message' do
Object.stub(to_s: 'some string')
expect {
Object.to_s('bogus')
}.to raise_error(ArgumentError)
end
end
end
end

0 comments on commit 0357303

Please sign in to comment.