Skip to content

Commit

Permalink
Provide object_double to create verifying doubles from object instances.
Browse files Browse the repository at this point in the history
  • Loading branch information
xaviershay committed Oct 23, 2013
1 parent 45c4524 commit 623e701
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 12 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Expand Up @@ -29,6 +29,8 @@ Enhancements:
ported from `rspec-fire` (Xavier Shay).
* `as_null_object` on a verifying double only responds to defined methods
(Xavier Shay).
* Provide `object_double` to create verified doubles of specific object
instances (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
1 change: 1 addition & 0 deletions features/.nav
Expand Up @@ -25,6 +25,7 @@
- verifying_doubles:
- introduction.feature
- class_doubles.feature
- object_doubles.feature
- dynamic_classes.feature
- outside_rspec:
- configuration.feature
Expand Down
65 changes: 65 additions & 0 deletions features/verifying_doubles/object_doubles.feature
@@ -0,0 +1,65 @@
Feature: Using an object double

`object_double` can be used to create a double from an existing "template"
object, from which it verifies that any stubbed methods on the double also
exist on the template. This is useful for objects that are readily
constructable, but may have far-reaching side-effects such as talking to a
database or external API. In this case, using a double rather than the real
thing allows you to focus on the communication patterns of the object's
interface without having to worry about accidentally causing side-effects.
Object doubles can also be used to verify methods defined on an object using
`method_missing`, which is not possible with `instance_double`.

In addition, `object_double` can be used with specific constant values, as
shown below. This is for niche situations, such as when dealing with
singleton objects.

Scenario: doubling an existing object
Given a file named "spec/user_spec.rb" with:
"""ruby
class User
# Don't want to accidentally trigger this!
def save; sleep 100; end
end
def save_user(user)
"saved!" if user.save
end
describe '#save_user' do
it 'renders message on success' do
user = object_double(User.new, :save => true)
expect(save_user(user)).to eq("saved!")
end
end
"""
When I run `rspec spec/user_spec.rb`
Then the examples should all pass


Scenario: doubling a constant object
Given a file named "spec/email_spec.rb" with:
"""ruby
require 'logger'
module MyApp
LOGGER = Logger.new("myapp")
end
class Email
def self.send_to(recipient)
MyApp::LOGGER.info("Sent to #{recipient}")
# other emailing logic
end
end
describe Email do
it 'logs a message when sending' do
logger = object_double("MyApp::LOGGER", :info => nil).as_stubbed_const
Email.send_to('hello@foo.com')
expect(logger).to have_received(:info).with("Sent to hello@foo.com")
end
end
"""
When I run `rspec spec/email_spec.rb`
Then the examples should all pass
32 changes: 28 additions & 4 deletions lib/rspec/mocks/example_methods.rb
@@ -1,4 +1,4 @@
require 'rspec/mocks/module_reference'
require 'rspec/mocks/object_reference'

module RSpec
module Mocks
Expand Down Expand Up @@ -42,7 +42,7 @@ def double(*args)
# allowed to be stubbed. In all other ways it behaves like a
# [double](double).
def instance_double(doubled_class, *args)
declare_verifying_double(InstanceVerifyingDouble, doubled_class, *args)
declare_instance_or_class_double(InstanceVerifyingDouble, doubled_class, *args)
end

# @overload class_double(doubled_class)
Expand All @@ -56,7 +56,27 @@ def instance_double(doubled_class, *args)
# allowed to be stubbed. In all other ways it behaves like a
# [double](double).
def class_double(doubled_class, *args)
declare_verifying_double(ClassVerifyingDouble, doubled_class, *args)
declare_instance_or_class_double(ClassVerifyingDouble, doubled_class, *args)
end

# @overload object_double(object_or_name)
# @overload object_double(object_or_name, stubs)
# @param object_or_name [String, Object]
# @param stubs [Hash] (optional) hash of message/return-value pairs
# @return ObjectVerifyingDouble
#
# Constructs a test double against a specific object. Only instance
# methods on the object are allowed to be stubbed. If a String argument
# is provided, it is assumed to reference a constant object which is used
# for verification. In all other ways it behaves like a [double](double).
def object_double(object_or_name, *args)
ref = if object_or_name.is_a?(String)
ModuleReference.new(object_or_name)
else
ObjectReference.new(object_or_name)
end

declare_verifying_double(ObjectVerifyingDouble, ref, *args)
end

# Disables warning messages about expectations being set on nil.
Expand Down Expand Up @@ -157,9 +177,13 @@ def self.included(klass)

private

def declare_verifying_double(type, constant_or_name, *args)
def declare_instance_or_class_double(type, constant_or_name, *args)
ref = ModuleReference.new(constant_or_name)

declare_verifying_double(type, ref, *args)
end

def declare_verifying_double(type, ref, *args)
if RSpec::Mocks.configuration.verify_doubled_constant_names? &&
!ref.defined?

Expand Down
2 changes: 1 addition & 1 deletion lib/rspec/mocks/method_reference.rb
Expand Up @@ -77,7 +77,7 @@ def find_method(m)
end

# @private
class ClassMethodReference < MethodReference
class ObjectMethodReference < MethodReference
private
def method_implemented?(m)
m.respond_to?(@method_name)
Expand Down
@@ -1,6 +1,27 @@
module RSpec
module Mocks

# An abstraction in front of objects so that non-loaded objects can be
# worked with. The null case is for concrete objects that are always
# loaded. See `ModuleReference` for an example of non-loaded objects.
class ObjectReference
def initialize(object)
@object = object
end

def name
@object.to_s
end

def defined?
true
end

def when_loaded
yield @object
end
end

# Provides a consistent interface for dealing with modules that may or may
# not be defined.
#
Expand Down
29 changes: 22 additions & 7 deletions lib/rspec/mocks/verifying_double.rb
Expand Up @@ -35,12 +35,9 @@ def __build_mock_proxy
end
end

# Similar to an InstanceVerifyingDouble, except that it verifies against
# public methods of the given class (i.e. the "class methods").
#
# Module needs to be in the inheritance chain for transferring nested
# constants to work.
class ClassVerifyingDouble < Module
# An awkward module necessary because we cannot otherwise have
# ClassVerifyingDouble inherit from Module and still share these methods.
module ObjectVerifyingDoubleMethods
include TestDouble
include VerifyingDouble

Expand All @@ -53,15 +50,33 @@ def initialize(doubled_module, *args)
def __build_mock_proxy
VerifyingProxy.new(self,
@doubled_module,
ClassMethodReference
ObjectMethodReference
)
end

def as_stubbed_const(options = {})
if @doubled_module.kind_of?(ObjectReference)
raise ArgumentError,
"Can not perform constant replacement with an object."
end

ConstantMutator.stub(@doubled_module.name, self, options)
self
end
end

# Similar to an InstanceVerifyingDouble, except that it verifies against
# public methods of the given object.
class ObjectVerifyingDouble
include ObjectVerifyingDoubleMethods
end

# Effectively the same as an ObjectVerifyingDouble (since a class is a type
# of object), except with Module in the inheritance chain so that
# transferring nested constants to work.
class ClassVerifyingDouble < Module
include ObjectVerifyingDoubleMethods
end

end
end
72 changes: 72 additions & 0 deletions spec/rspec/mocks/verifying_double_spec.rb
Expand Up @@ -3,6 +3,7 @@
class LoadedClass
M = :m
N = :n
INSTANCE = LoadedClass.new

def defined_instance_method; end
def self.defined_class_method; end
Expand Down Expand Up @@ -122,6 +123,18 @@ def prevents(&block)
prevents { o.undefined_method }
end
end

it 'cannot be constructed with a non-module object' do
expect {
instance_double(Object.new)
}.to raise_error(/Module or String expected/)
end

it 'can be constructed with a struct' do
o = instance_double(Struct.new(:defined_method), :defined_method => 1)

expect(o.defined_method).to eq(1)
end
end

describe 'class doubles' do
Expand Down Expand Up @@ -229,8 +242,67 @@ def prevents(&block)
prevents { o.undefined_method }
end
end

it 'cannot be constructed with a non-module object' do
expect {
class_double(Object.new)
}.to raise_error(/Module or String expected/)
end
end

describe 'object doubles' do
it 'replaces an unloaded constant' do
o = object_double("LoadedClass::NOINSTANCE").as_stubbed_const

expect(LoadedClass::NOINSTANCE).to eq(o)

expect(o).to receive(:undefined_instance_method)
o.undefined_instance_method
end

it 'replaces a constant by name and verifies instances methods' do
o = object_double("LoadedClass::INSTANCE").as_stubbed_const

expect(LoadedClass::INSTANCE).to eq(o)

prevents { expect(o).to receive(:undefined_instance_method) }
prevents { expect(o).to receive(:defined_class_method) }
prevents { o.defined_instance_method }

expect(o).to receive(:defined_instance_method)
o.defined_instance_method
end

it 'can create a double that matches the interface of any arbitrary object' do
o = object_double(LoadedClass.new)

prevents { expect(o).to receive(:undefined_instance_method) }
prevents { expect(o).to receive(:defined_class_method) }
prevents { o.defined_instance_method }

expect(o).to receive(:defined_instance_method)
o.defined_instance_method
end

it 'does not allow transferring constants to an object' do
expect {
object_double("LoadedClass::INSTANCE").
as_stubbed_const(:transfer_nested_constants => true)
}.to raise_error(/Cannot transfer nested constants/)
end

it 'does not allow as_stubbed_constant for real objects' do
expect {
object_double(LoadedClass.new).as_stubbed_const
}.to raise_error(/Can not perform constant replacement with an object/)
end

it 'is not a module' do
expect(object_double("LoadedClass::INSTANCE")).to_not be_a(Module)
end
end


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

Expand Down

0 comments on commit 623e701

Please sign in to comment.