Skip to content

Commit

Permalink
Clarify relationship between different method stub actions.
Browse files Browse the repository at this point in the history
In 2.13.0 only `and_yield` and `and_return` could be combined, since
that was the only combination case that was specified by an example.
This was a regression, as reported by a user in #230.

I tried here to fully specify all of the various combinations of stub 
actions. Notes:

* `and_return`, `and_raise` and `and_throw` are "terminal" actions
  in the sense that they terminate the method. They _must_ happen last
  and it is impossible to support more than one of these. Hence, we allow
  only one of these, and allow them to be overridden. We also return `nil`
  from these methods to discourage further stub configuration.
* `and_call_original` is a special case that doesn't make sense to be
  combined with any of the others. Once you've set it up, this causes
  any further instructions to raise an error.
* `and_yield` is treated as an "initial" action. Yielding doesn't exit
  a method the way the terminal actions do. It is the only initial action.
  Calling it multiple times sets up multiple yields.
* Setting a block implementation (possible by passing a block to almost
  any method on the fluent interface) sets the block as the "inner" action.
  It runs between the configured yields (if there are any) and the configured
  terminal action (if there is one). My thinking here is that in many cases,
  users use a block implementation to specify a return value, essentially
  making it like a terminal action (so it should come after the `and_yield`
  actions), but in other cases, the user may just use a block for a side
  effect, and may configure an terminal action as well. Only one block
  implementation is supported and it can be overridden.
  • Loading branch information
myronmarston committed Jun 18, 2013
1 parent 98bec84 commit 0073068
Show file tree
Hide file tree
Showing 3 changed files with 283 additions and 37 deletions.
4 changes: 4 additions & 0 deletions Changelog.md
Expand Up @@ -14,6 +14,10 @@ Enhancements:
Bug Fixes:

* Bypass RSpec::Mocks::Syntax when mass-assigning stubs via double(). (Paul Annesley)
* Allow a block implementation to be used in combination with
`and_yield`, `and_raise`, `and_return` or `and_throw`. This got fixed
in 2.13.1 but failed to get merged into master for the 2.14.0.rc1
release (Myron Marston).

### 2.14.0.rc1 / 2013-05-27
[full changelog](http://github.com/rspec/rspec-mocks/compare/v2.13.0...v2.14.0.rc1)
Expand Down
119 changes: 82 additions & 37 deletions lib/rspec/mocks/message_expectation.rb
Expand Up @@ -10,7 +10,7 @@ class MessageExpectation

# @private
def initialize(error_generator, expectation_ordering, expected_from, method_double,
expected_received_count=1, opts={}, &implementation)
expected_received_count=1, opts={}, &implementation_block)
@error_generator = error_generator
@error_generator.opts = opts
@expected_from = expected_from
Expand All @@ -24,10 +24,9 @@ def initialize(error_generator, expectation_ordering, expected_from, method_doub
@args_to_yield = []
@failed_fast = nil
@eval_context = nil
@implementation = implementation

@initial_implementation_logic = nil
@terminal_implementation_logic = nil
@implementation = Implementation.new
self.inner_implementation_action = implementation_block
end

# @private
Expand Down Expand Up @@ -78,10 +77,12 @@ def and_return(*values, &implementation)

if implementation
# TODO: deprecate `and_return { value }`
@implementation = implementation
self.inner_implementation_action = implementation
else
self.terminal_implementation_logic = AndReturnImplementation.new(values)
self.terminal_implementation_action = AndReturnImplementation.new(values)
end

nil
end

# Tells the object to delegate to the original unmodified method
Expand All @@ -99,7 +100,7 @@ def and_call_original
if @method_double.object.is_a?(RSpec::Mocks::TestDouble)
@error_generator.raise_only_valid_on_a_partial_mock(:and_call_original)
else
@implementation = @method_double.original_method
@implementation = AndCallOriginalImplementation.new(@method_double.original_method)
end
end

Expand Down Expand Up @@ -129,7 +130,8 @@ def and_raise(exception = RuntimeError, message = nil)
exception = message ? exception.exception(message) : exception.exception
end

@implementation = Proc.new { raise exception }
self.terminal_implementation_action = Proc.new { raise exception }
nil
end

# @overload and_throw(symbol)
Expand All @@ -143,7 +145,8 @@ def and_raise(exception = RuntimeError, message = nil)
# car.stub(:go).and_throw(:out_of_gas)
# car.stub(:go).and_throw(:out_of_gas, :level => 0.1)
def and_throw(*args)
@implementation = Proc.new { throw(*args) }
self.terminal_implementation_action = Proc.new { throw(*args) }
nil
end

# Tells the object to yield one or more args to a block when the message
Expand All @@ -155,7 +158,7 @@ def and_throw(*args)
def and_yield(*args, &block)
yield @eval_context = Object.new if block
@args_to_yield << args
self.initial_implementation_logic = AndYieldImplementation.new(@args_to_yield, @eval_context, @error_generator)
self.initial_implementation_action = AndYieldImplementation.new(@args_to_yield, @eval_context, @error_generator)
self
end

Expand All @@ -177,8 +180,8 @@ def invoke(parent_stub, *args, &block)
@order_group.handle_order_constraint self

begin
if @implementation
@implementation.call(*args, &block)
if implementation.present?
implementation.call(*args, &block)
elsif parent_stub
parent_stub.invoke(nil, *args, &block)
end
Expand Down Expand Up @@ -292,7 +295,7 @@ def raise_out_of_order_error
# cart.add(Book.new(:isbn => 1934356379))
# # => passes
def with(*args, &block)
@implementation = block if block_given? unless args.empty?
self.inner_implementation_action = block if block_given? unless args.empty?
@argument_list_matcher = ArgumentListMatcher.new(*args, &block)
self
end
Expand All @@ -304,7 +307,7 @@ def with(*args, &block)
#
# dealer.should_receive(:deal_card).exactly(10).times
def exactly(n, &block)
@implementation = block if block
self.inner_implementation_action = block
set_expected_received_count :exactly, n
self
end
Expand All @@ -320,7 +323,7 @@ def at_least(n, &block)
RSpec.deprecate "at_least(0) with should_receive", :replacement => "stub"
end

@implementation = block if block
self.inner_implementation_action = block
set_expected_received_count :at_least, n
self
end
Expand All @@ -332,7 +335,7 @@ def at_least(n, &block)
#
# dealer.should_receive(:deal_card).at_most(10).times
def at_most(n, &block)
@implementation = block if block
self.inner_implementation_action = block
set_expected_received_count :at_most, n
self
end
Expand All @@ -345,15 +348,15 @@ def at_most(n, &block)
# dealer.should_receive(:deal_card).at_least(10).times
# dealer.should_receive(:deal_card).at_most(10).times
def times(&block)
@implementation = block if block
self.inner_implementation_action = block
self
end


# Allows an expected message to be received any number of times.
def any_number_of_times(&block)
RSpec.deprecate "any_number_of_times", :replacement => "stub"
@implementation = block if block
self.inner_implementation_action = block
@expected_received_count = :any
self
end
Expand All @@ -374,7 +377,7 @@ def never
#
# car.should_receive(:go).once
def once(&block)
@implementation = block if block
self.inner_implementation_action = block
set_expected_received_count :exactly, 1
self
end
Expand All @@ -385,7 +388,7 @@ def once(&block)
#
# car.should_receive(:go).twice
def twice(&block)
@implementation = block if block
self.inner_implementation_action = block
set_expected_received_count :exactly, 2
self
end
Expand All @@ -398,7 +401,7 @@ def twice(&block)
# api.should_receive(:run).ordered
# api.should_receive(:finish).ordered
def ordered(&block)
@implementation = block if block
self.inner_implementation_action = block
@order_group.register(self)
@ordered = true
self
Expand Down Expand Up @@ -436,21 +439,16 @@ def set_expected_received_count(relativity, n)
end
end

def initial_implementation_logic=(logic)
@initial_implementation_logic = logic
update_implementation
def initial_implementation_action=(action)
implementation.initial_action = action
end

def terminal_implementation_logic=(logic)
@terminal_implementation_logic = logic
update_implementation
def inner_implementation_action=(action)
implementation.inner_action = action if action
end

def update_implementation
@implementation = Implementation.new(
@initial_implementation_logic,
@terminal_implementation_logic
).method(:call)
def terminal_implementation_action=(action)
implementation.terminal_action = action
end
end

Expand Down Expand Up @@ -480,7 +478,7 @@ def initialize(args_to_yield, eval_context, error_generator)
@error_generator = error_generator
end

def call(*args_to_ignore, block)
def call(*args_to_ignore, &block)
return if @args_to_yield.empty? && @eval_context.nil?

@error_generator.raise_missing_block_error @args_to_yield unless block
Expand All @@ -502,7 +500,7 @@ def initialize(values_to_return)
@values_to_return = values_to_return
end

def call(*args_to_ignore, block)
def call(*args_to_ignore, &block)
if @values_to_return.size > 1
@values_to_return.shift
else
Expand All @@ -515,12 +513,59 @@ def call(*args_to_ignore, block)
# any number of sub-implementations.
# @private
class Implementation
def initialize(*implementations)
@implementations = implementations.compact
attr_accessor :initial_action, :inner_action, :terminal_action

def call(*args, &block)
actions.map do |action|
action.call(*args, &block)
end.last
end

def present?
actions.any?
end

private

def actions
[initial_action, inner_action, terminal_action].compact
end
end

# Represents an `and_call_original` implementation.
# @private
class AndCallOriginalImplementation
def initialize(method)
@method = method
end

CannotModifyFurtherError = Class.new(StandardError)

def initial_action=(value)
raise cannot_modify_further_error
end

def inner_action=(value)
raise cannot_modify_further_error
end

def terminal_action=(value)
raise cannot_modify_further_error
end

def present?
true
end

def call(*args, &block)
@implementations.map { |i| i.call(*args, block) }.last
@method.call(*args, &block)
end

private

def cannot_modify_further_error
CannotModifyFurtherError.new "This method has already been configured " +
"to call the original implementation, and cannot be modified further."
end
end
end
Expand Down

0 comments on commit 0073068

Please sign in to comment.