Permalink
Browse files

Extract mock logic into TestDouble module.

This allows objects to be treated as a pure test double without needing to subclass RSpec::Mocks::Mock.

Closes #117.
  • Loading branch information...
1 parent c8a5ef5 commit af3f296ed24b25cb014b3e0b1afa9f0e90db8635 @myronmarston myronmarston committed Mar 20, 2012
@@ -64,7 +64,7 @@ def raise_wrong_arity_error(args_to_yield, arity)
def intro
if @name
"#{@declared_as} #{@name.inspect}"
- elsif Mock === @target
+ elsif TestDouble === @target
@declared_as
elsif Class === @target
"<#{@target.inspect} (class)>"
@@ -7,6 +7,7 @@
require 'rspec/mocks/methods'
require 'rspec/mocks/argument_matchers'
require 'rspec/mocks/proxy'
+require 'rspec/mocks/test_double'
require 'rspec/mocks/mock'
require 'rspec/mocks/argument_expectation'
require 'rspec/mocks/message_expectation'
@@ -27,7 +27,7 @@ def stubs
# @private
def visibility
- if Mock === @object
+ if TestDouble === @object
'public'
elsif object_singleton_class.private_method_defined?(@method_name)
'private'
@@ -125,7 +125,7 @@ def rspec_reset
def __mock_proxy
@mock_proxy ||= begin
- mp = if Mock === self
+ mp = if TestDouble === self
Proxy.new(self, @name, @options)
else
Proxy.new(self)
@@ -1,81 +1,7 @@
module RSpec
module Mocks
class Mock
- include Methods
-
- # Creates a new test double with a `name` (that will be used in error
- # messages only)
- def initialize(name=nil, stubs_and_options={})
- if name.is_a?(Hash) && stubs_and_options.empty?
- stubs_and_options = name
- @name = nil
- else
- @name = name
- end
- @options = extract_options(stubs_and_options)
- assign_stubs(stubs_and_options)
- end
-
- # This allows for comparing the mock to other objects that proxy such as
- # ActiveRecords belongs_to proxy objects. By making the other object run
- # the comparison, we're sure the call gets delegated to the proxy
- # target.
- def ==(other)
- other == __mock_proxy
- end
-
- # @private
- def inspect
- "#<#{self.class}:#{sprintf '0x%x', self.object_id} @name=#{@name.inspect}>"
- end
-
- # @private
- def to_s
- inspect.gsub('<','[').gsub('>',']')
- end
-
- alias_method :to_str, :to_s
-
- # @private
- def respond_to?(message, incl_private=false)
- __mock_proxy.null_object? && message != :to_ary ? true : super
- end
-
- private
-
- def method_missing(message, *args, &block)
- raise NoMethodError if message == :to_ary
- __mock_proxy.record_message_received(message, *args, &block)
- begin
- __mock_proxy.null_object? ? self : super
- rescue NameError
- __mock_proxy.raise_unexpected_message_error(message, *args)
- end
- end
-
- def extract_options(stubs_and_options)
- if stubs_and_options[:null_object]
- @null_object = stubs_and_options.delete(:null_object)
- RSpec.deprecate(%Q["double('name', :null_object => true)"], %Q["double('name').as_null_object"])
- end
- options = {}
- extract_option(stubs_and_options, options, :__declared_as, 'Mock')
- options
- end
-
- def extract_option(source, target, key, default=nil)
- if source[key]
- target[key] = source.delete(key)
- elsif default
- target[key] = default
- end
- end
-
- def assign_stubs(stubs)
- stubs.each_pair do |message, response|
- stub(message).and_return(response)
- end
- end
+ include TestDouble
end
end
end
@@ -0,0 +1,102 @@
+module RSpec
+ module Mocks
+ # Implements the methods needed for a pure test double. RSpec::Mocks::Mock
+ # includes this module, and it is provided for cases where you want a
+ # pure test double without subclassing RSpec::Mocks::Mock.
+ module TestDouble
+ include Methods
+
+ # Extends the TestDouble module onto the given object and
+ # initializes it as a test double.
+ #
+ # @example
+ #
+ # module = Module.new
+ # RSpec::Mocks::TestDouble.extend_onto(module, "MyMixin", :foo => "bar")
+ # module.foo #=> "bar"
+ def self.extend_onto(object, name=nil, stubs_and_options={})
+ object.extend self
+ object.send(:__initialize_as_test_double, name, stubs_and_options)
+ end
+
+ # Creates a new test double with a `name` (that will be used in error
+ # messages only)
+ def initialize(name=nil, stubs_and_options={})
+ __initialize_as_test_double(name, stubs_and_options)
+ end
+
+ # This allows for comparing the mock to other objects that proxy such as
+ # ActiveRecords belongs_to proxy objects. By making the other object run
+ # the comparison, we're sure the call gets delegated to the proxy
+ # target.
+ def ==(other)
+ other == __mock_proxy
+ end
+
+ # @private
+ def inspect
+ "#<#{self.class}:#{sprintf '0x%x', self.object_id} @name=#{@name.inspect}>"
+ end
+
+ # @private
+ def to_s
+ inspect.gsub('<','[').gsub('>',']')
+ end
+
+ alias_method :to_str, :to_s
+
+ # @private
+ def respond_to?(message, incl_private=false)
+ __mock_proxy.null_object? && message != :to_ary ? true : super
+ end
+
+ private
+
+ def __initialize_as_test_double(name=nil, stubs_and_options={})
+ if name.is_a?(Hash) && stubs_and_options.empty?
+ stubs_and_options = name
+ @name = nil
+ else
+ @name = name
+ end
+ @options = extract_options(stubs_and_options)
+ assign_stubs(stubs_and_options)
+ end
+
+ def method_missing(message, *args, &block)
+ raise NoMethodError if message == :to_ary
+ __mock_proxy.record_message_received(message, *args, &block)
+ begin
+ __mock_proxy.null_object? ? self : super
+ rescue NameError
+ __mock_proxy.raise_unexpected_message_error(message, *args)
+ end
+ end
+
+ def extract_options(stubs_and_options)
+ if stubs_and_options[:null_object]
+ @null_object = stubs_and_options.delete(:null_object)
+ RSpec.deprecate(%Q["double('name', :null_object => true)"], %Q["double('name').as_null_object"])
+ end
+ options = {}
+ extract_option(stubs_and_options, options, :__declared_as, 'Mock')
+ options
+ end
+
+ def extract_option(source, target, key, default=nil)
+ if source[key]
+ target[key] = source.delete(key)
+ elsif default
+ target[key] = default
+ end
+ end
+
+ def assign_stubs(stubs)
+ stubs.each_pair do |message, response|
+ stub(message).and_return(response)
+ end
+ end
+ end
+ end
+end
+
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+module RSpec
+ module Mocks
+ describe TestDouble do
+ before(:all) do
+ Module.class_eval do
+ private
+ def use; end
+ end
+ end
+
+ after(:all) do
+ Module.class_eval do
+ undef use
+ end
+ end
+
+ it 'can be extended onto a module to make it a pure test double that can mock private methods' do
+ double = Module.new
+ double.stub(:use)
+ expect { double.use }.to raise_error(/private method `use' called/)
+
+ double = Module.new { TestDouble.extend_onto(self) }
+ double.should_receive(:use).and_return(:ok)
+ double.use.should be(:ok)
+ end
+
+ it 'sets the test double name when a name is passed' do
+ double = Module.new { TestDouble.extend_onto(self, "MyDouble") }
+ expect { double.foo }.to raise_error(/Mock "MyDouble" received/)
+ end
+
+ it 'stubs the methods passed in the stubs hash' do
+ double = Module.new do
+ TestDouble.extend_onto(self, "MyDouble", :a => 5, :b => 10)
+ end
+
+ double.a.should eq(5)
+ double.b.should eq(10)
+ end
+
+ it 'indicates what type of test double it is in error messages' do
+ double = Module.new do
+ TestDouble.extend_onto(self, "A", :__declared_as => "ModuleMock")
+ end
+ expect { double.foo }.to raise_error(/ModuleMock "A"/)
+ end
+
+ it 'is declared as a mock by default' do
+ double = Module.new { TestDouble.extend_onto(self) }
+ expect { double.foo }.to raise_error(/Mock received/)
+ end
+ end
+ end
+end
+

0 comments on commit af3f296

Please sign in to comment.