module RSpec module Mocks class MessageExpectation # @private attr_reader :message attr_writer :expected_received_count, :method_block, :expected_from, :argument_expectation protected :expected_received_count=, :method_block=, :expected_from= attr_accessor :error_generator protected :error_generator, :error_generator= # @private def initialize(error_generator, expectation_ordering, expected_from, message, method_block, expected_received_count=1, opts={}, &implementation) @error_generator = error_generator @error_generator.opts = opts @expected_from = expected_from @message = message @method_block = method_block @return_block = nil @actual_received_count = 0 @expected_received_count = expected_received_count @argument_expectation = ArgumentExpectation.new(ArgumentMatchers::AnyArgsMatcher.new) @consecutive = false @exception_to_raise = nil @args_to_throw = [] @order_group = expectation_ordering @at_least = nil @at_most = nil @exactly = nil @args_to_yield = [] @failed_fast = nil @args_to_yield_were_cloned = false @return_block = implementation @eval_context = nil end # @private def build_child(expected_from, method_block, expected_received_count, opts={}) child = clone child.expected_from = expected_from child.method_block = method_block child.expected_received_count = expected_received_count child.clear_actual_received_count! new_gen = error_generator.clone new_gen.opts = opts child.error_generator = new_gen child.clone_args_to_yield(*@args_to_yield) child.argument_expectation = ArgumentExpectation.new(ArgumentMatchers::AnyArgsMatcher.new) child end # @private def expected_args @argument_expectation.args end # @overload and_return(value) # @overload and_return(first_value, second_value) # @overload and_return(&block) # # Tells the object to return a value when it receives the message. Given # more than one value, the first value is returned the first time the # message is received, the second value is returned the next time, etc, # etc. # # If the message is received more times than there are values, the last # value is received for every subsequent call. # # The block format is still supported, but is unofficially deprecated in # favor of just passing a block to the stub method. # # @example # # counter.stub(:count).and_return(1) # counter.count # => 1 # counter.count # => 1 # # counter.stub(:count).and_return(1,2,3) # counter.count # => 1 # counter.count # => 2 # counter.count # => 3 # counter.count # => 3 # counter.count # => 3 # # etc # # # Supported, but ... # counter.stub(:count).and_return { 1 } # counter.count # => 1 # # # ... this is prefered # counter.stub(:count) { 1 } # counter.count # => 1 def and_return(*values, &return_block) Kernel::raise AmbiguousReturnError unless @method_block.nil? case values.size when 0 then value = nil when 1 then value = values[0] else value = values @consecutive = true @expected_received_count = values.size if !ignoring_args? && @expected_received_count < values.size end @return_block = block_given? ? return_block : lambda { value } end # @overload and_raise # @overload and_raise(ExceptionClass) # @overload and_raise(exception_instance) # # Tells the object to raise an exception when the message is received. # # @note # # When you pass an exception class, the MessageExpectation will raise # an instance of it, creating it with `new`. If the exception class # initializer requires any parameters, you must pass in an instance and # not the class. # # @example # # car.stub(:go).and_raise # car.stub(:go).and_raise(OutOfGas) # car.stub(:go).and_raise(OutOfGas.new(2, :oz)) def and_raise(exception=Exception) @exception_to_raise = exception end # @overload and_throw(symbol) # @overload and_throw(symbol, object) # # Tells the object to throw a symbol (with the object if that form is # used) when the message is received. # # @example # # car.stub(:go).and_throw(:out_of_gas) # car.stub(:go).and_throw(:out_of_gas, :level => 0.1) def and_throw(symbol, object = nil) @args_to_throw << symbol @args_to_throw << object if object end # Tells the object to yield one or more args to a block when the message # is received. # # @example # # stream.stub(:open).and_yield(StringIO.new) def and_yield(*args, &block) if @args_to_yield_were_cloned @args_to_yield.clear @args_to_yield_were_cloned = false end if block @eval_context = Object.new @eval_context.extend RSpec::Mocks::InstanceExec yield @eval_context end @args_to_yield << args self end # @private def matches?(message, *args) @message == message and @argument_expectation.args_match?(*args) end # @private def invoke(*args, &block) if @expected_received_count == 0 @failed_fast = true @actual_received_count += 1 @error_generator.raise_expectation_error(@message, @expected_received_count, @actual_received_count, *args) end @order_group.handle_order_constraint self begin begin raise(@exception_to_raise) unless @exception_to_raise.nil? rescue ArgumentError => e raise e.exception(<<-MESSAGE) 'and_raise' can only accept an Exception class if an instance can be constructed with no arguments. #{@exception_to_raise.to_s}'s initialize method requires #{@exception_to_raise.instance_method(:initialize).arity} arguments, so you have to supply an instance instead. MESSAGE end Kernel::throw(*@args_to_throw) unless @args_to_throw.empty? default_return_val = if !@method_block.nil? invoke_method_block(*args, &block) elsif !@args_to_yield.empty? || @eval_context invoke_with_yield(&block) else nil end if @consecutive invoke_consecutive_return_block(*args, &block) elsif @return_block invoke_return_block(*args, &block) else default_return_val end ensure @actual_received_count += 1 end end # @private def called_max_times? @expected_received_count != :any && @expected_received_count > 0 && @actual_received_count >= @expected_received_count end # @private def matches_name_but_not_args(message, *args) @message == message and not @argument_expectation.args_match?(*args) end # @private def verify_messages_received generate_error unless expected_messages_received? || failed_fast? rescue RSpec::Mocks::MockExpectationError => error error.backtrace.insert(0, @expected_from) Kernel::raise error end # @private def expected_messages_received? ignoring_args? || matches_exact_count? || matches_at_least_count? || matches_at_most_count? end # @private def ignoring_args? @expected_received_count == :any end # @private def matches_at_least_count? @at_least && @actual_received_count >= @expected_received_count end # @private def matches_at_most_count? @at_most && @actual_received_count <= @expected_received_count end # @private def matches_exact_count? @expected_received_count == @actual_received_count end # @private def similar_messages @similar_messages ||= [] end # @private def advise(*args) similar_messages << args end # @private def generate_error if similar_messages.empty? @error_generator.raise_expectation_error(@message, @expected_received_count, @actual_received_count, *@argument_expectation.args) else @error_generator.raise_similar_message_args_error(self, *@similar_messages) end end def raise_out_of_order_error @error_generator.raise_out_of_order_error @message end # Constrains a stub or message expectation to invocations with specific # arguments. # # With a stub, if the message might be received with other args as well, # you should stub a default value first, and then stub or mock the same # message using `with` to constrain to specific arguments. # # A message expectation will fail if the message is received with different # arguments. # # @example # # cart.stub(:add) { :failure } # cart.stub(:add).with(Book.new(:isbn => 1934356379)) { :success } # cart.add(Book.new(:isbn => 1234567890)) # # => :failure # cart.add(Book.new(:isbn => 1934356379)) # # => :success # # cart.should_receive(:add).with(Book.new(:isbn => 1934356379)) { :success } # cart.add(Book.new(:isbn => 1234567890)) # # => failed expectation # cart.add(Book.new(:isbn => 1934356379)) # # => passes def with(*args, &block) @return_block = block if block_given? unless args.empty? @argument_expectation = ArgumentExpectation.new(*args, &block) self end # Constrain a message expectation to be received a specific number of # times. # # @example # # dealer.should_recieve(:deal_card).exactly(10).times def exactly(n, &block) @method_block = block if block set_expected_received_count :exactly, n self end # Constrain a message expectation to be received at least a specific # number of times. # # @example # # dealer.should_recieve(:deal_card).at_least(9).times def at_least(n, &block) @method_block = block if block set_expected_received_count :at_least, n self end # Constrain a message expectation to be received at most a specific # number of times. # # @example # # dealer.should_recieve(:deal_card).at_most(10).times def at_most(n, &block) @method_block = block if block set_expected_received_count :at_most, n self end # Syntactic sugar for `exactly`, `at_least` and `at_most` # # @example # # dealer.should_recieve(:deal_card).exactly(10).times # dealer.should_recieve(:deal_card).at_least(10).times # dealer.should_recieve(:deal_card).at_most(10).times def times(&block) @method_block = block if block self end # Allows an expected message to be received any number of times. def any_number_of_times(&block) @method_block = block if block @expected_received_count = :any self end # Expect a message not to be received at all. # # @example # # car.should_receive(:stop).never def never @expected_received_count = 0 self end # Expect a message to be received exactly one time. # # @example # # car.should_receive(:go).once def once(&block) @method_block = block if block set_expected_received_count :exactly, 1 self end # Expect a message to be received exactly two times. # # @example # # car.should_receive(:go).twice def twice(&block) @method_block = block if block set_expected_received_count :exactly, 2 self end # Expect messages to be received in a specific order. # # @example # # api.should_receive(:prepare).ordered # api.should_receive(:run).ordered # api.should_receive(:finish).ordered def ordered(&block) @method_block = block if block @order_group.register(self) @ordered = true self end # @private def negative_expectation_for?(message) return false end # @private def actual_received_count_matters? @at_least || @at_most || @exactly end # @private def increase_actual_received_count! @actual_received_count += 1 end protected def invoke_method_block(*args, &block) begin @method_block.call(*args, &block) rescue => detail @error_generator.raise_block_failed_error(@message, detail.message) end end def invoke_with_yield(&block) if block.nil? @error_generator.raise_missing_block_error @args_to_yield end value = nil @args_to_yield.each do |args_to_yield_this_time| if block.arity > -1 && args_to_yield_this_time.length != block.arity @error_generator.raise_wrong_arity_error args_to_yield_this_time, block.arity end value = eval_block(*args_to_yield_this_time, &block) end value end def eval_block(*args, &block) if @eval_context @eval_context.instance_exec(*args, &block) else block.call(*args) end end def invoke_consecutive_return_block(*args, &block) value = invoke_return_block(*args, &block) index = [@actual_received_count, value.size-1].min value[index] end def invoke_return_block(*args, &block) @return_block.arity == 0 ? @return_block.call(&block) : @return_block.call(*args, &block) end def clone_args_to_yield(*args) @args_to_yield = args.clone @args_to_yield_were_cloned = true end def failed_fast? @failed_fast end def set_expected_received_count(relativity, n) @at_least = (relativity == :at_least) @at_most = (relativity == :at_most) @exactly = (relativity == :exactly) @expected_received_count = case n when Numeric then n when :once then 1 when :twice then 2 end end def clear_actual_received_count! @actual_received_count = 0 end end # @private class NegativeMessageExpectation < MessageExpectation # @private def initialize(error_generator, expectation_ordering, expected_from, message, method_block) super(error_generator, expectation_ordering, expected_from, message, method_block, 0) end # @private def negative_expectation_for?(message) return @message == message end end end end