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 authored and JonRowe committed Jun 20, 2013
1 parent 727a9a4 commit f41cfcb
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: Bug Fixes:


* Bypass RSpec::Mocks::Syntax when mass-assigning stubs via double(). (Paul Annesley) * 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 ### 2.14.0.rc1 / 2013-05-27
[full changelog](http://github.com/rspec/rspec-mocks/compare/v2.13.0...v2.14.0.rc1) [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 # @private
def initialize(error_generator, expectation_ordering, expected_from, method_double, 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 = error_generator
@error_generator.opts = opts @error_generator.opts = opts
@expected_from = expected_from @expected_from = expected_from
Expand All @@ -24,10 +24,9 @@ def initialize(error_generator, expectation_ordering, expected_from, method_doub
@args_to_yield = [] @args_to_yield = []
@failed_fast = nil @failed_fast = nil
@eval_context = nil @eval_context = nil
@implementation = implementation


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


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


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

nil
end end


# Tells the object to delegate to the original unmodified method # 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) if @method_double.object.is_a?(RSpec::Mocks::TestDouble)
@error_generator.raise_only_valid_on_a_partial_mock(:and_call_original) @error_generator.raise_only_valid_on_a_partial_mock(:and_call_original)
else else
@implementation = @method_double.original_method @implementation = AndCallOriginalImplementation.new(@method_double.original_method)
end end
end end


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


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


# @overload and_throw(symbol) # @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)
# car.stub(:go).and_throw(:out_of_gas, :level => 0.1) # car.stub(:go).and_throw(:out_of_gas, :level => 0.1)
def and_throw(*args) def and_throw(*args)
@implementation = Proc.new { throw(*args) } self.terminal_implementation_action = Proc.new { throw(*args) }
nil
end end


# Tells the object to yield one or more args to a block when the message # 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) def and_yield(*args, &block)
yield @eval_context = Object.new if block yield @eval_context = Object.new if block
@args_to_yield << args @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 self
end end


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


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


@implementation = block if block self.inner_implementation_action = block
set_expected_received_count :at_least, n set_expected_received_count :at_least, n
self self
end end
Expand All @@ -332,7 +335,7 @@ def at_least(n, &block)
# #
# dealer.should_receive(:deal_card).at_most(10).times # dealer.should_receive(:deal_card).at_most(10).times
def at_most(n, &block) def at_most(n, &block)
@implementation = block if block self.inner_implementation_action = block
set_expected_received_count :at_most, n set_expected_received_count :at_most, n
self self
end 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_least(10).times
# dealer.should_receive(:deal_card).at_most(10).times # dealer.should_receive(:deal_card).at_most(10).times
def times(&block) def times(&block)
@implementation = block if block self.inner_implementation_action = block
self self
end end




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


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


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


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


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


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


@error_generator.raise_missing_block_error @args_to_yield unless block @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 @values_to_return = values_to_return
end end


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


def call(*args, &block) 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 end
end end
Expand Down

0 comments on commit f41cfcb

Please sign in to comment.