1
0
Fork 0
mirror of https://github.com/thoughtbot/shoulda-matchers.git synced 2022-11-09 12:01:38 -05:00

Add a small stubbing library

This provides a robust solution for temporarily stubbing (and
unstubbing) methods. It will be internally by the strong parameters and
delegation matchers.
This commit is contained in:
Elliot Winkler 2014-04-19 18:01:22 -06:00
parent c7b1505990
commit dfebd81af0
18 changed files with 904 additions and 0 deletions

View file

@ -1,4 +1,5 @@
require 'shoulda/matchers/assertion_error'
require 'shoulda/matchers/doublespeak'
require 'shoulda/matchers/error'
require 'shoulda/matchers/rails_shim'
require 'shoulda/matchers/warn'

View file

@ -0,0 +1,27 @@
require 'forwardable'
module Shoulda
module Matchers
module Doublespeak
class << self
extend Forwardable
def_delegators :world, :register_double_collection,
:with_doubles_activated
def world
@_world ||= World.new
end
end
end
end
end
require 'shoulda/matchers/doublespeak/double'
require 'shoulda/matchers/doublespeak/double_collection'
require 'shoulda/matchers/doublespeak/double_implementation_registry'
require 'shoulda/matchers/doublespeak/object_double'
require 'shoulda/matchers/doublespeak/proxy_implementation'
require 'shoulda/matchers/doublespeak/structs'
require 'shoulda/matchers/doublespeak/stub_implementation'
require 'shoulda/matchers/doublespeak/world'

View file

@ -0,0 +1,74 @@
module Shoulda
module Matchers
module Doublespeak
class Double
attr_reader :calls
def initialize(klass, method_name, implementation)
@klass = klass
@method_name = method_name
@implementation = implementation
@activated = false
@calls = []
end
def to_return(value = nil, &block)
if block
implementation.returns(&block)
else
implementation.returns(value)
end
end
def activate
unless @activated
store_original_method
replace_method_with_double
@activated = true
end
end
def deactivate
if @activated
restore_original_method
@activated = false
end
end
def record_call(args, block)
calls << MethodCall.new(args, block)
end
def call_original_method(object, args, block)
if original_method
original_method.bind(object).call(*args, &block)
end
end
private
attr_reader :klass, :method_name, :implementation, :original_method
def store_original_method
@original_method = klass.instance_method(method_name)
end
def replace_method_with_double
implementation = @implementation
double = self
klass.__send__(:define_method, method_name) do |*args, &block|
implementation.call(double, self, args, block)
end
end
def restore_original_method
original_method = @original_method
klass.__send__(:define_method, method_name) do |*args, &block|
original_method.bind(self).call(*args, &block)
end
end
end
end
end
end

View file

@ -0,0 +1,54 @@
module Shoulda
module Matchers
module Doublespeak
class DoubleCollection
def initialize(klass)
@klass = klass
@doubles_by_method_name = {}
end
def register_stub(method_name)
register_double(method_name, :stub)
end
def register_proxy(method_name)
register_double(method_name, :proxy)
end
def activate
doubles_by_method_name.each do |method_name, double|
double.activate
end
end
def deactivate
doubles_by_method_name.each do |method_name, double|
double.deactivate
end
end
def calls_to(method_name)
double = doubles_by_method_name[method_name]
if double
double.calls
else
[]
end
end
private
attr_reader :klass, :doubles_by_method_name
def register_double(method_name, implementation_type)
implementation =
DoubleImplementationRegistry.find(implementation_type)
double = Double.new(klass, method_name, implementation)
doubles_by_method_name[method_name] = double
double
end
end
end
end
end

View file

@ -0,0 +1,27 @@
module Shoulda
module Matchers
module Doublespeak
module DoubleImplementationRegistry
class << self
REGISTRY = {}
def find(type)
find_class!(type).create
end
def register(klass, type)
REGISTRY[type] = klass
end
private
def find_class!(type)
REGISTRY.fetch(type) do
raise ArgumentError, "No double implementation class found for '#{type}'"
end
end
end
end
end
end
end

View file

@ -0,0 +1,32 @@
module Shoulda
module Matchers
module Doublespeak
class ObjectDouble < BasicObject
attr_reader :calls
def initialize
@calls = []
@calls_by_method_name = {}
end
def calls_to(method_name)
@calls_by_method_name[method_name] || []
end
def respond_to?(name, include_private = nil)
true
end
def method_missing(method_name, *args, &block)
calls << MethodCallWithName.new(method_name, args, block)
(calls_by_method_name[method_name] ||= []) << MethodCall.new(args, block)
nil
end
private
attr_reader :calls_by_method_name
end
end
end
end

View file

@ -0,0 +1,30 @@
module Shoulda
module Matchers
module Doublespeak
class ProxyImplementation
extend Forwardable
DoubleImplementationRegistry.register(self, :proxy)
def_delegators :stub_implementation, :returns
def self.create
new(StubImplementation.new)
end
def initialize(stub_implementation)
@stub_implementation = stub_implementation
end
def call(double, object, args, block)
stub_implementation.call(double, object, args, block)
double.call_original_method(object, args, block)
end
private
attr_reader :stub_implementation
end
end
end
end

View file

@ -0,0 +1,8 @@
module Shoulda
module Matchers
module Doublespeak
MethodCall = Struct.new(:args, :block)
MethodCallWithName = Struct.new(:method_name, :args, :block)
end
end
end

View file

@ -0,0 +1,34 @@
module Shoulda
module Matchers
module Doublespeak
class StubImplementation
DoubleImplementationRegistry.register(self, :stub)
def self.create
new
end
def initialize
@implementation = proc { nil }
end
def returns(value = nil, &block)
if block
@implementation = block
else
@implementation = proc { value }
end
end
def call(double, object, args, block)
double.record_call(args, block)
implementation.call(object, args, block)
end
private
attr_reader :implementation
end
end
end
end

View file

@ -0,0 +1,38 @@
module Shoulda
module Matchers
module Doublespeak
class World
def register_double_collection(klass)
double_collection = DoubleCollection.new(klass)
double_collections_by_class[klass] = double_collection
double_collection
end
def with_doubles_activated
activate
yield
ensure
deactivate
end
private
def activate
double_collections_by_class.each do |klass, double_collection|
double_collection.activate
end
end
def deactivate
double_collections_by_class.each do |klass, double_collection|
double_collection.deactivate
end
end
def double_collections_by_class
@_double_collections_by_class ||= {}
end
end
end
end
end

View file

@ -0,0 +1,102 @@
require 'spec_helper'
module Shoulda::Matchers::Doublespeak
describe DoubleCollection do
describe '#register_stub' do
it 'calls DoubleImplementationRegistry.find correctly' do
double_collection = described_class.new(:klass)
DoubleImplementationRegistry.expects(:find).with(:stub)
double_collection.register_stub(:a_method)
end
it 'calls Double.new correctly' do
DoubleImplementationRegistry.stubs(:find).returns(:implementation)
double_collection = described_class.new(:klass)
Double.expects(:new).with(:klass, :a_method, :implementation)
double_collection.register_stub(:a_method)
end
end
describe '#register_proxy' do
it 'calls DoubleImplementationRegistry.find correctly' do
double_collection = described_class.new(:klass)
DoubleImplementationRegistry.expects(:find).with(:proxy)
double_collection.register_proxy(:a_method)
end
it 'calls Double.new correctly' do
DoubleImplementationRegistry.stubs(:find).returns(:implementation)
double_collection = described_class.new(:klass)
Double.expects(:new).with(:klass, :a_method, :implementation)
double_collection.register_proxy(:a_method)
end
end
describe '#activate' do
it 'replaces all registered methods with doubles' do
klass = create_class(first_method: 1, second_method: 2)
double_collection = described_class.new(klass)
double_collection.register_stub(:first_method)
double_collection.register_stub(:second_method)
double_collection.activate
instance = klass.new
expect(instance.first_method).to eq nil
expect(instance.second_method).to eq nil
end
end
describe '#deactivate' do
it 'restores the original methods that were doubled' do
klass = create_class(first_method: 1, second_method: 2)
double_collection = described_class.new(klass)
double_collection.register_stub(:first_method)
double_collection.register_stub(:second_method)
double_collection.activate
double_collection.deactivate
instance = klass.new
expect(instance.first_method).to eq 1
expect(instance.second_method).to eq 2
end
end
describe '#calls_to' do
it 'returns all calls to the given method' do
klass = create_class(a_method: nil)
double_collection = described_class.new(klass)
double_collection.register_stub(:a_method)
double_collection.activate
actual_calls = [
{ args: [:some, :args, :here] },
{ args: [:some, :args], block: -> { :whatever } }
]
instance = klass.new
instance.a_method(*actual_calls[0][:args])
instance.a_method(*actual_calls[1][:args], &actual_calls[1][:block])
calls = double_collection.calls_to(:a_method)
expect(calls[0].args).to eq actual_calls[0][:args]
expect(calls[1].args).to eq actual_calls[1][:args]
expect(calls[1].block).to eq actual_calls[1][:block]
end
it 'returns an empty array if the method has never been doubled' do
klass = create_class
double_collection = described_class.new(klass)
expect(double_collection.calls_to(:non_existent_method)).to eq []
end
end
def create_class(methods = {})
Class.new.tap do |klass|
methods.each do |name, value|
klass.__send__(:define_method, name) { |*args| value }
end
end
end
end
end

View file

@ -0,0 +1,21 @@
require 'spec_helper'
module Shoulda::Matchers::Doublespeak
describe DoubleImplementationRegistry do
describe '.find' do
it 'returns an instance of StubImplementation if given :stub' do
expect(described_class.find(:stub)).to be_a(StubImplementation)
end
it 'returns ProxyImplementation if given :proxy' do
expect(described_class.find(:proxy)).to be_a(ProxyImplementation)
end
it 'raises an ArgumentError if not given a registered implementation' do
expect {
expect(described_class.find(:something_else))
}.to raise_error(ArgumentError)
end
end
end
end

View file

@ -0,0 +1,144 @@
require 'spec_helper'
module Shoulda::Matchers::Doublespeak
describe Double do
describe '#to_return' do
it 'tells its implementation to call the given block' do
sent_block = -> { }
actual_block = nil
implementation = stub
implementation.singleton_class.__send__(:define_method, :returns) do |&block|
actual_block = block
end
double = described_class.new(:klass, :a_method, implementation)
double.to_return(&sent_block)
expect(actual_block).to eq sent_block
end
it 'tells its implementation to return the given value' do
implementation = mock()
implementation.expects(:returns).with(:implementation)
double = described_class.new(:klass, :a_method, implementation)
double.to_return(:implementation)
end
it 'prefers a block over a non-block' do
sent_block = -> { }
actual_block = nil
implementation = stub
implementation.singleton_class.__send__(:define_method, :returns) do |&block|
actual_block = block
end
double = described_class.new(:klass, :a_method, implementation)
double.to_return(:value, &sent_block)
expect(actual_block).to eq sent_block
end
end
describe '#activate' do
it 'replaces the method with an implementation' do
implementation = stub
klass = create_class(a_method: 42)
instance = klass.new
double = described_class.new(klass, :a_method, implementation)
args = [:any, :args]
block = -> {}
implementation.expects(:call).with(double, instance, args, block)
double.activate
instance.a_method(*args, &block)
end
end
describe '#deactivate' do
it 'restores the original method after being doubled' do
implementation = stub(call: nil)
klass = create_class(a_method: 42)
instance = klass.new
double = described_class.new(klass, :a_method, implementation)
double.activate
double.deactivate
expect(instance.a_method).to eq 42
end
it 'still restores the original method if #activate was called twice' do
implementation = stub(call: nil)
klass = create_class(a_method: 42)
instance = klass.new
double = described_class.new(klass, :a_method, implementation)
double.activate
double.activate
double.deactivate
expect(instance.a_method).to eq 42
end
it 'does nothing if the method has not been doubled' do
implementation = stub(call: nil)
klass = create_class(a_method: 42)
instance = klass.new
double = described_class.new(klass, :a_method, implementation)
double.deactivate
expect(instance.a_method).to eq 42
end
end
describe '#record_call' do
it 'stores the arguments and block given to the method in calls' do
double = described_class.new(:klass, :a_method, :implementation)
calls = [
[:any, :args], :block,
[:more, :args]
]
double.record_call(calls[0][0], calls[0][1])
double.record_call(calls[1][0], nil)
expect(double.calls[0].args).to eq calls[0][0]
expect(double.calls[0].block).to eq calls[0][1]
expect(double.calls[1].args).to eq calls[1][0]
end
end
describe '#call_original_method' do
it 'binds the stored method object to the class and calls it with the given args and block' do
klass = create_class(a_method: nil)
instance = klass.new
actual_args = actual_block = method_called = nil
expected_args = [:one, :two, :three]
expected_block = -> { }
double = described_class.new(klass, :a_method, :implementation)
klass.__send__(:define_method, :a_method) do |*args, &block|
actual_args = expected_args
actual_block = expected_block
method_called = true
end
double.activate
double.call_original_method(instance, expected_args, expected_block)
expect(expected_args).to eq actual_args
expect(expected_block).to eq actual_block
expect(method_called).to eq true
end
it 'does nothing if no method has been stored' do
double = described_class.new(:klass, :a_method, :implementation)
expect {
double.call_original_method(:instance, [:any, :args], nil)
}.not_to raise_error
end
end
def create_class(methods = {})
Class.new.tap do |klass|
methods.each do |name, value|
klass.__send__(:define_method, name) { |*args| value }
end
end
end
end
end

View file

@ -0,0 +1,77 @@
require 'spec_helper'
module Shoulda::Matchers::Doublespeak
describe ObjectDouble do
it 'responds to any method' do
double = described_class.new
expect(double.respond_to?(:foo)).to be_true
expect(double.respond_to?(:bar)).to be_true
expect(double.respond_to?(:baz)).to be_true
end
it 'returns nil from any method call' do
double = described_class.new
expect(double.foo).to be_nil
expect(double.bar).to be_nil
expect(double.baz).to be_nil
end
it 'records every method call' do
double = described_class.new
block = -> { :some_return_value }
double.foo
double.bar(42)
double.baz(:zing, :zang, &block)
expect(double.calls.size).to eq 3
double.calls[0].tap do |call|
expect(call.args).to eq []
expect(call.block).to eq nil
end
double.calls[1].tap do |call|
expect(call.args).to eq [42]
expect(call.block).to eq nil
end
double.calls[2].tap do |call|
expect(call.args).to eq [:zing, :zang]
expect(call.block).to eq block
end
end
describe '#calls_to' do
it 'returns all of the invocations of the given method and their arguments/block' do
double = described_class.new
block = -> { :some_return_value }
double.foo
double.foo(42)
double.foo(:zing, :zang, &block)
double.some_other_method(:doesnt_matter)
calls = double.calls_to(:foo)
expect(calls.size).to eq 3
calls[0].tap do |call|
expect(call.args).to eq []
expect(call.block).to eq nil
end
calls[1].tap do |call|
expect(call.args).to eq [42]
expect(call.block).to eq nil
end
calls[2].tap do |call|
expect(call.args).to eq [:zing, :zang]
expect(call.block).to eq block
end
end
it 'returns an empty array if the given method was never called' do
double = described_class.new
expect(double.calls_to(:unknown_method)).to eq []
end
end
end
end

View file

@ -0,0 +1,40 @@
require 'spec_helper'
module Shoulda::Matchers::Doublespeak
describe ProxyImplementation do
describe '#returns' do
it 'delegates to its stub_implementation' do
stub_implementation = build_stub_implementation
stub_implementation.expects(:returns).with(:value)
implementation = described_class.new(stub_implementation)
implementation.returns(:value)
end
end
describe '#call' do
it 'delegates to its stub_implementation' do
stub_implementation = build_stub_implementation
double = build_double
stub_implementation.expects(:call).with(double, :object, :args, :block)
implementation = described_class.new(stub_implementation)
implementation.call(double, :object, :args, :block)
end
it 'calls #call_original_method on the double' do
stub_implementation = build_stub_implementation
implementation = described_class.new(stub_implementation)
double = build_double
double.expects(:call_original_method).with(:object, :args, :block)
implementation.call(double, :object, :args, :block)
end
end
def build_stub_implementation
stub(returns: nil, call: nil)
end
def build_double
stub(call_original_method: nil)
end
end
end

View file

@ -0,0 +1,88 @@
require 'spec_helper'
module Shoulda::Matchers::Doublespeak
describe StubImplementation do
describe '#call' do
it 'calls #record_call on the double' do
implementation = described_class.new
double = build_double
double.expects(:record_call).with(:args, :block)
implementation.call(double, :object, :args, :block)
end
context 'if no explicit implementation was set' do
it 'returns nil' do
implementation = described_class.new
double = build_double
return_value = implementation.call(double, :object, :args, :block)
expect(return_value).to eq nil
end
end
context 'if the implementation was set as a value' do
it 'returns the set return value' do
implementation = described_class.new
implementation.returns(42)
double = build_double
return_value = implementation.call(double, :object, :args, :block)
expect(return_value).to eq 42
end
end
context 'if the implementation was set as a block' do
it 'calls the block with the object and args/block passed to the method' do
double = build_double
expected_object, expected_args, expected_block = :object, :args, :block
actual_object, actual_args, actual_block = []
implementation = described_class.new
implementation.returns do |object, args, block|
actual_object, actual_args, actual_block = object, args, block
end
implementation.call(
double,
expected_object,
expected_args,
expected_block
)
expect(actual_object).to eq expected_object
expect(actual_args).to eq expected_args
expect(actual_block).to eq expected_block
end
it 'returns the return value of the block' do
implementation = described_class.new
implementation.returns { 42 }
double = build_double
return_value = implementation.call(double, :object, :args, :block)
expect(return_value).to eq 42
end
end
context 'if the implementation was set as both a value and a block' do
it 'prefers the block over the value' do
implementation = described_class.new
implementation.returns(:something_else) { 42 }
double = build_double
return_value = implementation.call(double, :object, :args, :block)
expect(return_value).to eq 42
end
end
end
def build_double
stub(record_call: nil)
end
end
end

View file

@ -0,0 +1,88 @@
require 'spec_helper'
module Shoulda::Matchers::Doublespeak
describe World do
describe '#register_double_collection' do
it 'calls DoubleCollection.new with the given class' do
DoubleCollection.expects(:new).with(:klass)
world = described_class.new
world.register_double_collection(:klass)
end
it 'returns the newly created DoubleCollection' do
double_collection = Object.new
DoubleCollection.stubs(:new).with(:klass).returns(double_collection)
world = described_class.new
expect(world.register_double_collection(:klass)).to be double_collection
end
end
describe '#with_doubles_activated' do
it 'installs all doubles, yields the block, then uninstalls them all' do
block_called = false
double_collections = Array.new(3) do
stub.tap do |double_collection|
sequence = sequence('with_doubles_activated')
double_collection.expects(:activate).in_sequence(sequence)
double_collection.expects(:deactivate).in_sequence(sequence)
end
end
world = described_class.new
DoubleCollection.stubs(:new).
with(:klass1).
returns(double_collections[0])
DoubleCollection.stubs(:new).
with(:klass2).
returns(double_collections[1])
DoubleCollection.stubs(:new).
with(:klass3).
returns(double_collections[2])
world.register_double_collection(:klass1)
world.register_double_collection(:klass2)
world.register_double_collection(:klass3)
world.with_doubles_activated { block_called = true }
expect(block_called).to eq true
end
it 'still makes sure to uninstall all doubles even if the block raises an error' do
double_collection = stub()
double_collection.stubs(:activate)
double_collection.expects(:deactivate)
world = described_class.new
DoubleCollection.stubs(:new).returns(double_collection)
world.register_double_collection(:klass)
begin
world.with_doubles_activated { raise 'error' }
rescue RuntimeError
end
end
it 'does not allow multiple DoubleCollections to be registered that represent the same class' do
double_collections = [stub, stub]
sequence = sequence('with_doubles_activated')
double_collections[0].expects(:activate).never
double_collections[0].expects(:deactivate).never
double_collections[1].expects(:activate).in_sequence(sequence)
double_collections[1].expects(:deactivate).in_sequence(sequence)
world = described_class.new
DoubleCollection.stubs(:new).
returns(double_collections[0]).then.
returns(double_collections[1])
world.register_double_collection(:klass1)
world.register_double_collection(:klass1)
world.with_doubles_activated { }
end
end
end
end

View file

@ -0,0 +1,19 @@
require 'spec_helper'
module Shoulda::Matchers
describe Doublespeak do
describe '.register_double_collection' do
it 'delegates to its world' do
Doublespeak.world.expects(:register_double_collection).with(:klass)
described_class.register_double_collection(:klass)
end
end
describe '.with_doubles_activated' do
it 'delegates to its world' do
Doublespeak.world.expects(:with_doubles_activated)
described_class.with_doubles_activated
end
end
end
end