Doublespeak: Only store original method once

If a method for a class is doubled more than once within the same test
run, the original implementation of that method will change from double
to double, even if the double is deactivated correctly. Say we have two
distinct tests that both double the same method. Here is how that method
will be overridden as it goes along:

    A) START: original method
    B) ACTIVATE (1st test): method is doubled
    C) DEACTIVATE (1st test): calls original method (A)
    D) ACTIVATE (2nd test): original method (C) stored; method is again
                            doubled
    E) DEACTIVATE (2nd test): calls original method (C)

With this commit, this changes to:

    A) START: original method
    B) ACTIVATE (1st test): method is doubled
    C) DEACTIVATE (1st test): calls original method (A)
    D) ACTIVATE (2nd test): original method not stored again; method is
                            again doubled
    E) DEACTIVATE (2nd test): calls original method (A)
This commit is contained in:
Elliot Winkler 2015-02-28 22:03:49 -07:00
parent 5e67a8305b
commit 755b3142a5
6 changed files with 185 additions and 50 deletions

View File

@ -5,7 +5,8 @@ module Shoulda
class Double class Double
attr_reader :calls attr_reader :calls
def initialize(klass, method_name, implementation) def initialize(world, klass, method_name, implementation)
@world = world
@klass = klass @klass = klass
@method_name = method_name @method_name = method_name
@implementation = implementation @implementation = implementation
@ -41,17 +42,20 @@ module Shoulda
end end
def call_original_method(call) def call_original_method(call)
if original_method unbound_method = world.original_method_for(klass, call.method_name)
original_method.bind(call.object).call(*call.args, &call.block)
if unbound_method
unbound_method.bind(call.object).call(*call.args, &call.block)
end end
end end
protected protected
attr_reader :klass, :method_name, :implementation, :original_method attr_reader :world, :klass, :method_name, :implementation,
:original_method
def store_original_method def store_original_method
@original_method = klass.instance_method(method_name) world.store_original_method_for(klass, method_name)
end end
def replace_method_with_double def replace_method_with_double
@ -72,8 +76,10 @@ module Shoulda
end end
def restore_original_method def restore_original_method
original_method = @original_method original_method = world.original_method_for(klass, method_name)
klass.__send__(:remove_method, method_name) klass.__send__(:remove_method, method_name)
klass.__send__(:define_method, method_name) do |*args, &block| klass.__send__(:define_method, method_name) do |*args, &block|
original_method.bind(self).call(*args, &block) original_method.bind(self).call(*args, &block)
end end

View File

@ -3,7 +3,8 @@ module Shoulda
module Doublespeak module Doublespeak
# @private # @private
class DoubleCollection class DoubleCollection
def initialize(klass) def initialize(world, klass)
@world = world
@klass = klass @klass = klass
@doubles_by_method_name = {} @doubles_by_method_name = {}
end end
@ -40,12 +41,12 @@ module Shoulda
protected protected
attr_reader :klass, :doubles_by_method_name attr_reader :world, :klass, :doubles_by_method_name
def register_double(method_name, implementation_type) def register_double(method_name, implementation_type)
implementation = implementation =
DoubleImplementationRegistry.find(implementation_type) DoubleImplementationRegistry.find(implementation_type)
double = Double.new(klass, method_name, implementation) double = Double.new(world, klass, method_name, implementation)
doubles_by_method_name[method_name] = double doubles_by_method_name[method_name] = double
double double
end end

View File

@ -4,7 +4,19 @@ module Shoulda
# @private # @private
class World class World
def double_collection_for(klass) def double_collection_for(klass)
double_collections_by_class[klass] ||= DoubleCollection.new(klass) double_collections_by_class[klass] ||=
DoubleCollection.new(self, klass)
end
def store_original_method_for(klass, method_name)
original_methods_for_class(klass)[method_name] ||=
klass.instance_method(method_name)
end
def original_method_for(klass, method_name)
if original_methods_by_class.key?(klass)
original_methods_by_class[klass][method_name]
end
end end
def with_doubles_activated def with_doubles_activated
@ -31,6 +43,14 @@ module Shoulda
def double_collections_by_class def double_collections_by_class
@_double_collections_by_class ||= {} @_double_collections_by_class ||= {}
end end
def original_methods_by_class
@_original_methods_by_class ||= {}
end
def original_methods_for_class(klass)
original_methods_by_class[klass] ||= {}
end
end end
end end
end end

View File

@ -5,7 +5,7 @@ module Shoulda::Matchers::Doublespeak
describe '#register_stub' do describe '#register_stub' do
it 'calls DoubleImplementationRegistry.find correctly' do it 'calls DoubleImplementationRegistry.find correctly' do
allow(DoubleImplementationRegistry).to receive(:find) allow(DoubleImplementationRegistry).to receive(:find)
double_collection = described_class.new(:klass) double_collection = described_class.new(build_world, :klass)
double_collection.register_stub(:a_method) double_collection.register_stub(:a_method)
@ -13,24 +13,25 @@ module Shoulda::Matchers::Doublespeak
end end
it 'calls Double.new correctly' do it 'calls Double.new correctly' do
world = build_world
allow(DoubleImplementationRegistry). allow(DoubleImplementationRegistry).
to receive(:find). to receive(:find).
and_return(:implementation) and_return(:implementation)
allow(Double).to receive(:new) allow(Double).to receive(:new)
double_collection = described_class.new(:klass) double_collection = described_class.new(world, :klass)
double_collection.register_stub(:a_method) double_collection.register_stub(:a_method)
expect(Double). expect(Double).
to have_received(:new). to have_received(:new).
with(:klass, :a_method, :implementation) with(world, :klass, :a_method, :implementation)
end end
end end
describe '#register_proxy' do describe '#register_proxy' do
it 'calls DoubleImplementationRegistry.find correctly' do it 'calls DoubleImplementationRegistry.find correctly' do
allow(DoubleImplementationRegistry).to receive(:find) allow(DoubleImplementationRegistry).to receive(:find)
double_collection = described_class.new(:klass) double_collection = described_class.new(build_world, :klass)
double_collection.register_proxy(:a_method) double_collection.register_proxy(:a_method)
@ -40,24 +41,25 @@ module Shoulda::Matchers::Doublespeak
end end
it 'calls Double.new correctly' do it 'calls Double.new correctly' do
world = build_world
allow(DoubleImplementationRegistry). allow(DoubleImplementationRegistry).
to receive(:find). to receive(:find).
and_return(:implementation) and_return(:implementation)
allow(Double).to receive(:new) allow(Double).to receive(:new)
double_collection = described_class.new(:klass) double_collection = described_class.new(world, :klass)
double_collection.register_proxy(:a_method) double_collection.register_proxy(:a_method)
expect(Double). expect(Double).
to have_received(:new). to have_received(:new).
with(:klass, :a_method, :implementation) with(world, :klass, :a_method, :implementation)
end end
end end
describe '#activate' do describe '#activate' do
it 'replaces all registered methods with doubles' do it 'replaces all registered methods with doubles' do
klass = create_class(first_method: 1, second_method: 2) klass = create_class(first_method: 1, second_method: 2)
double_collection = described_class.new(klass) double_collection = described_class.new(build_world, klass)
double_collection.register_stub(:first_method) double_collection.register_stub(:first_method)
double_collection.register_stub(:second_method) double_collection.register_stub(:second_method)
@ -72,7 +74,7 @@ module Shoulda::Matchers::Doublespeak
describe '#deactivate' do describe '#deactivate' do
it 'restores the original methods that were doubled' do it 'restores the original methods that were doubled' do
klass = create_class(first_method: 1, second_method: 2) klass = create_class(first_method: 1, second_method: 2)
double_collection = described_class.new(klass) double_collection = described_class.new(build_world, klass)
double_collection.register_stub(:first_method) double_collection.register_stub(:first_method)
double_collection.register_stub(:second_method) double_collection.register_stub(:second_method)
@ -88,7 +90,7 @@ module Shoulda::Matchers::Doublespeak
describe '#calls_to' do describe '#calls_to' do
it 'returns all calls to the given method' do it 'returns all calls to the given method' do
klass = create_class(a_method: nil) klass = create_class(a_method: nil)
double_collection = described_class.new(klass) double_collection = described_class.new(build_world, klass)
double_collection.register_stub(:a_method) double_collection.register_stub(:a_method)
double_collection.activate double_collection.activate
@ -108,7 +110,7 @@ module Shoulda::Matchers::Doublespeak
it 'returns an empty array if the method has never been doubled' do it 'returns an empty array if the method has never been doubled' do
klass = create_class klass = create_class
double_collection = described_class.new(klass) double_collection = described_class.new(build_world, klass)
expect(double_collection.calls_to(:non_existent_method)).to eq [] expect(double_collection.calls_to(:non_existent_method)).to eq []
end end
end end
@ -120,5 +122,9 @@ module Shoulda::Matchers::Doublespeak
end end
end end
end end
def build_world
Shoulda::Matchers::Doublespeak::World.new
end
end end
end end

View File

@ -11,14 +11,24 @@ module Shoulda::Matchers::Doublespeak
implementation.singleton_class.__send__(:define_method, :returns) do |&block| implementation.singleton_class.__send__(:define_method, :returns) do |&block|
actual_block = block actual_block = block
end end
double = described_class.new(:klass, :a_method, implementation) double = described_class.new(
:a_world,
:klass,
:a_method,
implementation
)
double.to_return(&sent_block) double.to_return(&sent_block)
expect(actual_block).to eq sent_block expect(actual_block).to eq sent_block
end end
it 'tells its implementation to return the given value' do it 'tells its implementation to return the given value' do
implementation = build_implementation implementation = build_implementation
double = described_class.new(:klass, :a_method, implementation) double = described_class.new(
:a_world,
:klass,
:a_method,
implementation
)
double.to_return(:implementation) double.to_return(:implementation)
expect(implementation).to have_received(:returns).with(:implementation) expect(implementation).to have_received(:returns).with(:implementation)
@ -32,7 +42,12 @@ module Shoulda::Matchers::Doublespeak
implementation.singleton_class.__send__(:define_method, :returns) do |&block| implementation.singleton_class.__send__(:define_method, :returns) do |&block|
actual_block = block actual_block = block
end end
double = described_class.new(:klass, :a_method, implementation) double = described_class.new(
:a_world,
:klass,
:a_method,
implementation
)
double.to_return(:value, &sent_block) double.to_return(:value, &sent_block)
expect(actual_block).to eq sent_block expect(actual_block).to eq sent_block
end end
@ -44,7 +59,12 @@ module Shoulda::Matchers::Doublespeak
method_name = :a_method method_name = :a_method
klass = create_class(method_name => :some_return_value) klass = create_class(method_name => :some_return_value)
instance = klass.new instance = klass.new
double = described_class.new(klass, method_name, implementation) double = described_class.new(
build_world,
klass,
method_name,
implementation
)
args = [:any, :args] args = [:any, :args]
block = -> {} block = -> {}
call = MethodCall.new( call = MethodCall.new(
@ -66,10 +86,17 @@ module Shoulda::Matchers::Doublespeak
describe '#deactivate' do describe '#deactivate' do
it 'restores the original method after being doubled' do it 'restores the original method after being doubled' do
implementation = build_implementation
klass = create_class(a_method: 42) klass = create_class(a_method: 42)
world = build_world(
original_method_for: klass.instance_method(:a_method)
)
instance = klass.new instance = klass.new
double = described_class.new(klass, :a_method, implementation) double = described_class.new(
world,
klass,
:a_method,
build_implementation
)
double.activate double.activate
double.deactivate double.deactivate
@ -77,10 +104,18 @@ module Shoulda::Matchers::Doublespeak
end end
it 'still restores the original method if #activate was called twice' do it 'still restores the original method if #activate was called twice' do
implementation = build_implementation method_name = :a_method
klass = create_class(a_method: 42) klass = create_class(method_name => 42)
world = build_world(
original_method_for: klass.instance_method(:a_method)
)
instance = klass.new instance = klass.new
double = described_class.new(klass, :a_method, implementation) double = described_class.new(
world,
klass,
:a_method,
build_implementation
)
double.activate double.activate
double.activate double.activate
@ -89,10 +124,14 @@ module Shoulda::Matchers::Doublespeak
end end
it 'does nothing if the method has not been doubled' do it 'does nothing if the method has not been doubled' do
implementation = build_implementation
klass = create_class(a_method: 42) klass = create_class(a_method: 42)
instance = klass.new instance = klass.new
double = described_class.new(klass, :a_method, implementation) double = described_class.new(
:a_world,
klass,
:a_method,
build_implementation
)
double.deactivate double.deactivate
expect(instance.a_method).to eq 42 expect(instance.a_method).to eq 42
@ -101,7 +140,12 @@ module Shoulda::Matchers::Doublespeak
describe '#record_call' do describe '#record_call' do
it 'adds the given call to the list of calls' do it 'adds the given call to the list of calls' do
double = described_class.new(:a_klass, :a_method, :an_implementation) double = described_class.new(
:a_world,
:a_klass,
:a_method,
:an_implementation
)
double.record_call(:some_call) double.record_call(:some_call)
expect(double.calls.last).to eq :some_call expect(double.calls.last).to eq :some_call
end end
@ -109,35 +153,82 @@ module Shoulda::Matchers::Doublespeak
describe '#call_original_method' do describe '#call_original_method' do
it 'binds the stored method object to the given object and calls it with the given args and block' do it 'binds the stored method object to the given object and calls it with the given args and block' do
klass = create_class
instance = klass.new
actual_args = actual_block = method_called = nil
expected_args = [:one, :two, :three] expected_args = [:one, :two, :three]
expected_block = -> { } expected_block = -> { }
actual_args = actual_block = method_called = nil
method_name = :a_method
klass = create_class
klass.__send__(:define_method, method_name) do |*args, &block|
actual_args = args
actual_block = block
method_called = true
end
world = build_world(
original_method_for: klass.instance_method(method_name)
)
instance = klass.new
call = double('call', call = double('call',
object: instance, object: instance,
method_name: method_name,
args: expected_args, args: expected_args,
block: expected_block block: expected_block
) )
double = described_class.new(klass, :a_method, :an_implementation) double = described_class.new(
world,
klass.__send__(:define_method, :a_method) do |*args, &block| klass,
actual_args = expected_args method_name,
actual_block = expected_block :an_implementation
method_called = true )
end
double.activate double.activate
double.call_original_method(call) double.call_original_method(call)
expect(expected_args).to eq actual_args expect(actual_args).to eq expected_args
expect(expected_block).to eq actual_block expect(actual_block).to eq expected_block
expect(method_called).to eq true expect(method_called).to eq true
end end
it 'does nothing if no method has been stored' do it 'does nothing if no method has been stored' do
double = described_class.new(:klass, :a_method, :an_implementation) method_name = :a_method
expect { double.call_original_method(:a_call) }.not_to raise_error world = build_world(original_method_for: nil)
call = double('call', method_name: method_name)
double = described_class.new(
world,
:klass,
method_name,
:an_implementation
)
expect { double.call_original_method(call) }.not_to raise_error
end
it 'does not store the original method multiple times when a method is doubled multiple times' do
world = Shoulda::Matchers::Doublespeak::World.new
klass = create_class(a_method: :some_return_value)
method_name = :a_method
doubles = 2.times.map do
described_class.new(
world,
klass,
method_name,
build_implementation
)
end
instance = klass.new
doubles[0].activate
was_called = false
klass.__send__(:define_method, method_name) do
was_called = true
end
doubles[1].activate
doubles.each(&:deactivate)
instance.__send__(method_name)
expect(was_called).to be false
end end
end end
@ -152,5 +243,13 @@ module Shoulda::Matchers::Doublespeak
def build_implementation def build_implementation
double('implementation', returns: nil, call: nil) double('implementation', returns: nil, call: nil)
end end
def build_world(methods = {})
defaults = {
original_method_for: nil,
store_original_method_for: nil
}
double('world', defaults.merge(methods))
end
end end
end end

View File

@ -10,16 +10,19 @@ module Shoulda::Matchers::Doublespeak
world.double_collection_for(:klass) world.double_collection_for(:klass)
world.double_collection_for(:klass) world.double_collection_for(:klass)
expect(DoubleCollection).to have_received(:new).with(:klass).once expect(DoubleCollection).
to have_received(:new).
with(world, :klass).
once
end end
it 'returns the created DoubleCollection' do it 'returns the created DoubleCollection' do
world = described_class.new
double_collection = build_double_collection double_collection = build_double_collection
allow(DoubleCollection). allow(DoubleCollection).
to receive(:new). to receive(:new).
with(:klass). with(world, :klass).
and_return(double_collection) and_return(double_collection)
world = described_class.new
expect(world.double_collection_for(:klass)).to be double_collection expect(world.double_collection_for(:klass)).to be double_collection
end end
@ -40,7 +43,7 @@ module Shoulda::Matchers::Doublespeak
double_collections.zip(klasses).each do |double_collection, klass| double_collections.zip(klasses).each do |double_collection, klass|
allow(DoubleCollection). allow(DoubleCollection).
to receive(:new). to receive(:new).
with(klass). with(world, klass).
and_return(double_collection) and_return(double_collection)
world.double_collection_for(klass) world.double_collection_for(klass)
end end