require 'mocha/method_matcher'
require 'mocha/parameters_matcher'
require 'mocha/expectation_error'
require 'mocha/return_values'
require 'mocha/exception_raiser'
require 'mocha/yield_parameters'
require 'mocha/is_a'
require 'mocha/in_state_ordering_constraint'
require 'mocha/change_state_side_effect'
require 'mocha/cardinality'
module Mocha # :nodoc:
# Methods on expectations returned from Mock#expects, Mock#stubs, Object#expects and Object#stubs.
class Expectation
# :call-seq: times(range) -> expectation
#
# Modifies expectation so that the number of calls to the expected method must be within a specific +range+.
#
# +range+ can be specified as an exact integer or as a range of integers
# object = mock()
# object.expects(:expected_method).times(3)
# 3.times { object.expected_method }
# # => verify succeeds
#
# object = mock()
# object.expects(:expected_method).times(3)
# 2.times { object.expected_method }
# # => verify fails
#
# object = mock()
# object.expects(:expected_method).times(2..4)
# 3.times { object.expected_method }
# # => verify succeeds
#
# object = mock()
# object.expects(:expected_method).times(2..4)
# object.expected_method
# # => verify fails
def times(range)
@cardinality = Cardinality.times(range)
self
end
# :call-seq: twice() -> expectation
#
# Modifies expectation so that the expected method must be called exactly twice.
# object = mock()
# object.expects(:expected_method).twice
# object.expected_method
# object.expected_method
# # => verify succeeds
#
# object = mock()
# object.expects(:expected_method).twice
# object.expected_method
# object.expected_method
# object.expected_method
# # => verify fails
#
# object = mock()
# object.expects(:expected_method).twice
# object.expected_method
# # => verify fails
def twice
@cardinality = Cardinality.exactly(2)
self
end
# :call-seq: once() -> expectation
#
# Modifies expectation so that the expected method must be called exactly once.
# Note that this is the default behaviour for an expectation, but you may wish to use it for clarity/emphasis.
# object = mock()
# object.expects(:expected_method).once
# object.expected_method
# # => verify succeeds
#
# object = mock()
# object.expects(:expected_method).once
# object.expected_method
# object.expected_method
# # => verify fails
#
# object = mock()
# object.expects(:expected_method).once
# # => verify fails
def once
@cardinality = Cardinality.exactly(1)
self
end
# :call-seq: never() -> expectation
#
# Modifies expectation so that the expected method must never be called.
# object = mock()
# object.expects(:expected_method).never
# object.expected_method
# # => verify fails
#
# object = mock()
# object.expects(:expected_method).never
# # => verify succeeds
def never
@cardinality = Cardinality.exactly(0)
self
end
# :call-seq: at_least(minimum_number_of_times) -> expectation
#
# Modifies expectation so that the expected method must be called at least a +minimum_number_of_times+.
# object = mock()
# object.expects(:expected_method).at_least(2)
# 3.times { object.expected_method }
# # => verify succeeds
#
# object = mock()
# object.expects(:expected_method).at_least(2)
# object.expected_method
# # => verify fails
def at_least(minimum_number_of_times)
@cardinality = Cardinality.at_least(minimum_number_of_times)
self
end
# :call-seq: at_least_once() -> expectation
#
# Modifies expectation so that the expected method must be called at least once.
# object = mock()
# object.expects(:expected_method).at_least_once
# object.expected_method
# # => verify succeeds
#
# object = mock()
# object.expects(:expected_method).at_least_once
# # => verify fails
def at_least_once
at_least(1)
self
end
# :call-seq: at_most(maximum_number_of_times) -> expectation
#
# Modifies expectation so that the expected method must be called at most a +maximum_number_of_times+.
# object = mock()
# object.expects(:expected_method).at_most(2)
# 2.times { object.expected_method }
# # => verify succeeds
#
# object = mock()
# object.expects(:expected_method).at_most(2)
# 3.times { object.expected_method }
# # => verify fails
def at_most(maximum_number_of_times)
@cardinality = Cardinality.at_most(maximum_number_of_times)
self
end
# :call-seq: at_most_once() -> expectation
#
# Modifies expectation so that the expected method must be called at most once.
# object = mock()
# object.expects(:expected_method).at_most_once
# object.expected_method
# # => verify succeeds
#
# object = mock()
# object.expects(:expected_method).at_most_once
# 2.times { object.expected_method }
# # => verify fails
def at_most_once()
at_most(1)
self
end
# :call-seq: with(*expected_parameters, &matching_block) -> expectation
#
# Modifies expectation so that the expected method must be called with +expected_parameters+.
# object = mock()
# object.expects(:expected_method).with(:param1, :param2)
# object.expected_method(:param1, :param2)
# # => verify succeeds
#
# object = mock()
# object.expects(:expected_method).with(:param1, :param2)
# object.expected_method(:param3)
# # => verify fails
# May be used with parameter matchers in Mocha::ParameterMatchers.
#
# If a +matching_block+ is given, the block is called with the parameters passed to the expected method.
# The expectation is matched if the block evaluates to +true+.
# object = mock()
# object.expects(:expected_method).with() { |value| value % 4 == 0 }
# object.expected_method(16)
# # => verify succeeds
#
# object = mock()
# object.expects(:expected_method).with() { |value| value % 4 == 0 }
# object.expected_method(17)
# # => verify fails
def with(*expected_parameters, &matching_block)
@parameters_matcher = ParametersMatcher.new(expected_parameters, &matching_block)
self
end
# :call-seq: yields(*parameters) -> expectation
#
# Modifies expectation so that when the expected method is called, it yields with the specified +parameters+.
# object = mock()
# object.expects(:expected_method).yields('result')
# yielded_value = nil
# object.expected_method { |value| yielded_value = value }
# yielded_value # => 'result'
# May be called multiple times on the same expectation for consecutive invocations. Also see Expectation#then.
# object = mock()
# object.stubs(:expected_method).yields(1).then.yields(2)
# yielded_values_from_first_invocation = []
# yielded_values_from_second_invocation = []
# object.expected_method { |value| yielded_values_from_first_invocation << value } # first invocation
# object.expected_method { |value| yielded_values_from_second_invocation << value } # second invocation
# yielded_values_from_first_invocation # => [1]
# yielded_values_from_second_invocation # => [2]
def yields(*parameters)
@yield_parameters.add(*parameters)
self
end
# :call-seq: multiple_yields(*parameter_groups) -> expectation
#
# Modifies expectation so that when the expected method is called, it yields multiple times per invocation with the specified +parameter_groups+.
# object = mock()
# object.expects(:expected_method).multiple_yields(['result_1', 'result_2'], ['result_3'])
# yielded_values = []
# object.expected_method { |*values| yielded_values << values }
# yielded_values # => [['result_1', 'result_2'], ['result_3]]
# May be called multiple times on the same expectation for consecutive invocations. Also see Expectation#then.
# object = mock()
# object.stubs(:expected_method).multiple_yields([1, 2], [3]).then.multiple_yields([4], [5, 6])
# yielded_values_from_first_invocation = []
# yielded_values_from_second_invocation = []
# object.expected_method { |*values| yielded_values_from_first_invocation << values } # first invocation
# object.expected_method { |*values| yielded_values_from_second_invocation << values } # second invocation
# yielded_values_from_first_invocation # => [[1, 2], [3]]
# yielded_values_from_second_invocation # => [[4], [5, 6]]
def multiple_yields(*parameter_groups)
@yield_parameters.multiple_add(*parameter_groups)
self
end
# :call-seq: returns(value) -> expectation
# returns(*values) -> expectation
#
# Modifies expectation so that when the expected method is called, it returns the specified +value+.
# object = mock()
# object.stubs(:stubbed_method).returns('result')
# object.stubbed_method # => 'result'
# object.stubbed_method # => 'result'
# If multiple +values+ are given, these are returned in turn on consecutive calls to the method.
# object = mock()
# object.stubs(:stubbed_method).returns(1, 2)
# object.stubbed_method # => 1
# object.stubbed_method # => 2
# May be called multiple times on the same expectation. Also see Expectation#then.
# object = mock()
# object.stubs(:expected_method).returns(1, 2).then.returns(3)
# object.expected_method # => 1
# object.expected_method # => 2
# object.expected_method # => 3
# May be called in conjunction with Expectation#raises on the same expectation.
# object = mock()
# object.stubs(:expected_method).returns(1, 2).then.raises(Exception)
# object.expected_method # => 1
# object.expected_method # => 2
# object.expected_method # => raises exception of class Exception1
# Note that in Ruby a method returning multiple values is exactly equivalent to a method returning an Array of those values.
# object = mock()
# object.stubs(:expected_method).returns([1, 2])
# x, y = object.expected_method
# x # => 1
# y # => 2
def returns(*values)
@return_values += ReturnValues.build(*values)
self
end
# :call-seq: raises(exception = RuntimeError, message = nil) -> expectation
#
# Modifies expectation so that when the expected method is called, it raises the specified +exception+ with the specified +message+ i.e. calls Kernel#raise(exception, message).
# object = mock()
# object.expects(:expected_method).raises(Exception, 'message')
# object.expected_method # => raises exception of class Exception and with message 'message'
# Note that if you have a custom exception class with extra constructor parameters, you can pass in an instance of the exception (just as you can for Kernel#raise).
# object = mock()
# object.expects(:expected_method).raises(MyException.new('message', 1, 2, 3))
# object.expected_method # => raises the specified instance of MyException
# May be called multiple times on the same expectation. Also see Expectation#then.
# object = mock()
# object.stubs(:expected_method).raises(Exception1).then.raises(Exception2)
# object.expected_method # => raises exception of class Exception1
# object.expected_method # => raises exception of class Exception2
# May be called in conjunction with Expectation#returns on the same expectation.
# object = mock()
# object.stubs(:expected_method).raises(Exception).then.returns(2, 3)
# object.expected_method # => raises exception of class Exception1
# object.expected_method # => 2
# object.expected_method # => 3
def raises(exception = RuntimeError, message = nil)
@return_values += ReturnValues.new(ExceptionRaiser.new(exception, message))
self
end
# :call-seq: then() -> expectation
# then(state_machine.is(state)) -> expectation
#
# then() is used as syntactic sugar to improve readability. It has no effect on state of the expectation.
# object = mock()
# object.stubs(:expected_method).returns(1, 2).then.raises(Exception).then.returns(4)
# object.expected_method # => 1
# object.expected_method # => 2
# object.expected_method # => raises exception of class Exception
# object.expected_method # => 4
#
# then(state_machine.is(state)) is used to change the +state_machine+ to the specified +state+ when the invocation occurs.
#
# See also API#states, StateMachine and Expectation#when.
# power = states('power').starts_as('off')
#
# radio = mock('radio')
# radio.expects(:switch_on).then(power.is('on'))
# radio.expects(:select_channel).with('BBC Radio 4').when(power.is('on'))
# radio.expects(:adjust_volume).with(+5).when(power.is('on'))
# radio.expects(:select_channel).with('BBC World Service').when(power.is('on'))
# radio.expects(:adjust_volume).with(-5).when(power.is('on'))
# radio.expects(:switch_off).then(power.is('off'))
def then(*parameters)
if parameters.length == 1
state = parameters.first
add_side_effect(ChangeStateSideEffect.new(state))
end
self
end
# :call-seq: when(state_machine.is(state)) -> exception
#
# Constrains the expectation to occur only when the +state_machine+ is in the named +state+.
#
# See also API#states, StateMachine#starts_as and Expectation#then.
# power = states('power').starts_as('off')
#
# radio = mock('radio')
# radio.expects(:switch_on).then(power.is('on'))
# radio.expects(:select_channel).with('BBC Radio 4').when(power.is('on'))
# radio.expects(:adjust_volume).with(+5).when(power.is('on'))
# radio.expects(:select_channel).with('BBC World Service').when(power.is('on'))
# radio.expects(:adjust_volume).with(-5).when(power.is('on'))
# radio.expects(:switch_off).then(power.is('off'))
def when(state_predicate)
add_ordering_constraint(InStateOrderingConstraint.new(state_predicate))
self
end
# :call-seq: in_sequence(*sequences) -> expectation
#
# Constrains this expectation so that it must be invoked at the current point in the sequence.
#
# To expect a sequence of invocations, write the expectations in order and add the in_sequence(sequence) clause to each one.
#
# Expectations in a sequence can have any invocation count.
#
# If an expectation in a sequence is stubbed, rather than expected, it can be skipped in the sequence.
#
# See also API#sequence.
# breakfast = sequence('breakfast')
#
# egg = mock('egg')
# egg.expects(:crack).in_sequence(breakfast)
# egg.expects(:fry).in_sequence(breakfast)
# egg.expects(:eat).in_sequence(breakfast)
def in_sequence(*sequences)
sequences.each { |sequence| add_in_sequence_ordering_constraint(sequence) }
self
end
# :stopdoc:
attr_reader :backtrace
def initialize(mock, expected_method_name, backtrace = nil)
@mock = mock
@method_matcher = MethodMatcher.new(expected_method_name.to_sym)
@parameters_matcher = ParametersMatcher.new
@ordering_constraints = []
@side_effects = []
@cardinality, @invocation_count = Cardinality.exactly(1), 0
@return_values = ReturnValues.new
@yield_parameters = YieldParameters.new
@backtrace = backtrace || caller
end
def add_ordering_constraint(ordering_constraint)
@ordering_constraints << ordering_constraint
end
def add_in_sequence_ordering_constraint(sequence)
sequence.constrain_as_next_in_sequence(self)
end
def add_side_effect(side_effect)
@side_effects << side_effect
end
def perform_side_effects
@side_effects.each { |side_effect| side_effect.perform }
end
def in_correct_order?
@ordering_constraints.all? { |ordering_constraint| ordering_constraint.allows_invocation_now? }
end
def matches_method?(method_name)
@method_matcher.match?(method_name)
end
def match?(actual_method_name, *actual_parameters)
@method_matcher.match?(actual_method_name) && @parameters_matcher.match?(actual_parameters) && in_correct_order?
end
def invocations_allowed?
@cardinality.invocations_allowed?(@invocation_count)
end
def satisfied?
@cardinality.satisfied?(@invocation_count)
end
def invoke
@invocation_count += 1
perform_side_effects()
if block_given? then
@yield_parameters.next_invocation.each do |yield_parameters|
yield(*yield_parameters)
end
end
@return_values.next
end
def verified?(assertion_counter = nil)
assertion_counter.increment if assertion_counter && @cardinality.needs_verifying?
@cardinality.verified?(@invocation_count)
end
def used?
@cardinality.used?(@invocation_count)
end
def mocha_inspect
message = "#{@cardinality.mocha_inspect}, "
message << case @invocation_count
when 0 then "not yet invoked"
when 1 then "already invoked once"
when 2 then "already invoked twice"
else "already invoked #{@invocation_count} times"
end
message << ": "
message << method_signature
message << "; #{@ordering_constraints.map { |oc| oc.mocha_inspect }.join("; ")}" unless @ordering_constraints.empty?
message
end
def method_signature
"#{@mock.mocha_inspect}.#{@method_matcher.mocha_inspect}#{@parameters_matcher.mocha_inspect}"
end
# :startdoc:
end
end