require 'ruby2_keywords' require 'mocha/method_matcher' require 'mocha/parameters_matcher' require 'mocha/expectation_error' require 'mocha/return_values' require 'mocha/exception_raiser' require 'mocha/thrower' require 'mocha/yield_parameters' require 'mocha/is_a' require 'mocha/in_state_ordering_constraint' require 'mocha/change_state_side_effect' require 'mocha/cardinality' require 'mocha/configuration' require 'mocha/block_matcher' require 'mocha/backtrace_filter' module Mocha # Methods on expectations returned from {Mock#expects}, {Mock#stubs}, {ObjectMethods#expects} and {ObjectMethods#stubs}. class Expectation # Modifies expectation so that the number of calls to the expected method must be within a specific +range+. # # @param [Range,Integer] range specifies the allowable range in the number of expected invocations. # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @example Specifying a specific number of expected invocations. # 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 # # @example Specifying a range in the number of expected invocations. # 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.times(range) self end # Modifies expectation so that the expected method must be called exactly twice. # # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @example Expected method must be invoked 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 # => unexpected invocation # # object = mock() # object.expects(:expected_method).twice # object.expected_method # # => verify fails def twice @cardinality.exactly(2) self end # 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. # # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @example Expected method must be invoked exactly once. # 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 # => unexpected invocation # # object = mock() # object.expects(:expected_method).once # # => verify fails def once @cardinality.exactly(1) self end # Modifies expectation so that the expected method must never be called. # # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @example Expected method must never be called. # object = mock() # object.expects(:expected_method).never # object.expected_method # => unexpected invocation # # object = mock() # object.expects(:expected_method).never # # => verify succeeds def never @cardinality.exactly(0) self end # Modifies expectation so that the expected method must be called at least a +minimum_number_of_times+. # # @param [Integer] minimum_number_of_times minimum number of expected invocations. # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @example Expected method must be called at least twice. # 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.at_least(minimum_number_of_times) self end # Modifies expectation so that the expected method must be called at least once. # # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @example 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) end # Modifies expectation so that the expected method must be called at most a +maximum_number_of_times+. # # @param [Integer] maximum_number_of_times maximum number of expected invocations. # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @example Expected method must be called at most twice. # 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 } # => unexpected invocation def at_most(maximum_number_of_times) @cardinality.at_most(maximum_number_of_times) self end # Modifies expectation so that the expected method must be called at most once. # # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @example 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 } # => unexpected invocation def at_most_once at_most(1) end # Modifies expectation so that the expected method must be called with +expected_parameters_or_matchers+. # # May be used with Ruby literals or variables for exact matching or with parameter matchers for less-specific matching, e.g. {ParameterMatchers#includes}, {ParameterMatchers#has_key}, etc. See {ParameterMatchers} for a list of all available parameter matchers. # # Positional arguments were separated from keyword arguments in Ruby v3 (see {https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0 this article}). In relation to this a new configuration option ({Configuration#strict_keyword_argument_matching=}) is available in Ruby >= 2.7. # # When {Configuration#strict_keyword_argument_matching=} is set to +false+ (which is currently the default), a positional +Hash+ and a set of keyword arguments passed to {#with} are treated the same for the purposes of parameter matching. However, a deprecation warning will be displayed if a positional +Hash+ matches a set of keyword arguments or vice versa. This is because {Configuration#strict_keyword_argument_matching=} will default to +true+ in the future. # # When {Configuration#strict_keyword_argument_matching=} is set to +true+, an actual positional +Hash+ will not match an expected set of keyword arguments; and vice versa, an actual set of keyword arguments will not match an expected positional +Hash+, i.e. the parameter matching is stricter. # # @see ParameterMatchers # @see Configuration#strict_keyword_argument_matching= # # @param [*Array] expected_parameters_or_matchers expected parameter values or parameter matchers. # @yield optional block specifying custom matching. # @yieldparam [*Array] actual_parameters parameters with which expected method was invoked. # @yieldreturn [Boolean] +true+ if +actual_parameters+ are acceptable. # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @example Expected method must be called with exact parameter values. # 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 # # @example Expected method must be called with parameters matching parameter matchers. # object = mock() # object.expects(:expected_method).with(includes('string2'), anything) # object.expected_method(['string1', 'string2'], 'any-old-value') # # => verify succeeds # # object = mock() # object.expects(:expected_method).with(includes('string2'), anything) # object.expected_method(['string1'], 'any-old-value') # # => verify fails # # @example Loose keyword argument matching (default) # # class Example # def foo(a, bar:); end # end # # example = Example.new # example.expects(:foo).with('a', bar: 'b') # example.foo('a', { bar: 'b' }) # # This passes the test, but would result in an ArgumentError in practice # # @example Strict keyword argument matching # # Mocha.configure do |c| # c.strict_keyword_argument_matching = true # end # # class Example # def foo(a, bar:); end # end # # example = Example.new # example.expects(:foo).with('a', bar: 'b') # example.foo('a', { bar: 'b' }) # # This now fails as expected # # @example Expected method must be called with a value divisible by 4. # 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_or_matchers, &matching_block) @parameters_matcher = ParametersMatcher.new(expected_parameters_or_matchers, self, &matching_block) self end ruby2_keywords(:with) # Modifies expectation so that the expected method must be called with a block. # # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @example Expected method must be called with a block. # object = mock() # object.expects(:expected_method).with_block_given # object.expected_method { 1 + 1 } # # => verify succeeds # # object = mock() # object.expects(:expected_method).with_block_given # object.expected_method # # => verify fails def with_block_given @block_matcher = BlockMatchers::BlockGiven.new self end # Modifies expectation so that the expected method must be called without a block. # # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @example Expected method must be called without a block. # object = mock() # object.expects(:expected_method).with_no_block_given # object.expected_method # # => verify succeeds # # object = mock() # object.expects(:expected_method).with_block_given # object.expected_method { 1 + 1 } # # => verify fails def with_no_block_given @block_matcher = BlockMatchers::NoBlockGiven.new self end # Modifies expectation so that when the expected method is called, it yields to the block with the specified +parameters+. # # If no +parameters+ are specified, it yields to the block without any parameters. # # If no block is provided, the method will still attempt to yield resulting in a +LocalJumpError+. Note that this is what would happen if a "real" (non-mock) method implementation tried to yield to a non-existent block. # # May be called multiple times on the same expectation for consecutive invocations. # # @param [*Array] parameters parameters to be yielded. # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # @see #then # # @example Yield when expected method is invoked. # benchmark = mock() # benchmark.expects(:measure).yields # yielded = false # benchmark.measure { yielded = true } # yielded # => true # # @example Yield parameters when expected method is invoked. # fibonacci = mock() # fibonacci.expects(:next_pair).yields(0, 1) # sum = 0 # fibonacci.next_pair { |first, second| sum = first + second } # sum # => 1 # # @example Yield different parameters on different invocations of the expected method. # fibonacci = mock() # fibonacci.expects(:next_pair).yields(0, 1).then.yields(1, 1) # sum = 0 # fibonacci.next_pair { |first, second| sum = first + second } # sum # => 1 # fibonacci.next_pair { |first, second| sum = first + second } # sum # => 2 def yields(*parameters) multiple_yields(parameters) end # Modifies expectation so that when the expected method is called, it yields multiple times per invocation with the specified +parameter_groups+. # # If no block is provided, the method will still attempt to yield resulting in a +LocalJumpError+. Note that this is what would happen if a "real" (non-mock) method implementation tried to yield to a non-existent block. # # @param [*Array] parameter_groups each element of +parameter_groups+ should iself be an +Array+ representing the parameters to be passed to the block for a single yield. Any element of +parameter_groups+ that is not an +Array+ is wrapped in an +Array+. # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # @see #then # # @example When +foreach+ is called, the stub will invoke the block twice, the first time it passes ['row1_col1', 'row1_col2'] as the parameters, and the second time it passes ['row2_col1', ''] as the parameters. # csv = mock() # csv.expects(:foreach).with("path/to/file.csv").multiple_yields(['row1_col1', 'row1_col2'], ['row2_col1', '']) # rows = [] # csv.foreach { |row| rows << row } # rows # => [['row1_col1', 'row1_col2'], ['row2_col1', '']] # # @example Yield different groups of parameters on different invocations of the expected method. Simulating a situation where the CSV file at 'path/to/file.csv' has been modified between the two calls to +foreach+. # csv = mock() # csv.stubs(:foreach).with("path/to/file.csv").multiple_yields(['old_row1_col1', 'old_row1_col2'], ['old_row2_col1', '']).then.multiple_yields(['new_row1_col1', ''], ['new_row2_col1', 'new_row2_col2']) # rows_from_first_invocation = [] # rows_from_second_invocation = [] # csv.foreach { |row| rows_from_first_invocation << row } # first invocation # csv.foreach { |row| rows_from_second_invocation << row } # second invocation # rows_from_first_invocation # => [['old_row1_col1', 'old_row1_col2'], ['old_row2_col1', '']] # rows_from_second_invocation # => [['new_row1_col1', ''], ['new_row2_col1', 'new_row2_col2']] def multiple_yields(*parameter_groups) @yield_parameters.add(*parameter_groups) self end # Modifies expectation so that when the expected method is called, it returns the specified +value+. # # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # @see #then # # @overload def returns(value) # @param [Object] value value to return on invocation of expected method. # @overload def returns(*values) # @param [*Array] values values to return on consecutive invocations of expected method. # # @example Return the same value on every invocation. # object = mock() # object.stubs(:stubbed_method).returns('result') # object.stubbed_method # => 'result' # object.stubbed_method # => 'result' # # @example Return a different value on consecutive invocations. # object = mock() # object.stubs(:stubbed_method).returns(1, 2) # object.stubbed_method # => 1 # object.stubbed_method # => 2 # # @example Alternative way to return a different value on consecutive invocations. # object = mock() # object.stubs(:expected_method).returns(1, 2).then.returns(3) # object.expected_method # => 1 # object.expected_method # => 2 # object.expected_method # => 3 # # @example May be called in conjunction with {#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 # # @example 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 # 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)+. # # @param [Class,Exception,String,#exception] exception exception to be raised or message to be passed to RuntimeError. # @param [String] message exception message. # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @see Kernel#raise # @see #then # # @overload def raises # @overload def raises(exception) # @overload def raises(exception, message) # # @example Raise specified exception if expected method is invoked. # object = stub() # object.stubs(:expected_method).raises(Exception, 'message') # object.expected_method # => raises exception of class Exception and with message 'message' # # @example Raise custom exception with extra constructor parameters by passing in an instance of the exception. # object = stub() # object.stubs(:expected_method).raises(MyException.new('message', 1, 2, 3)) # object.expected_method # => raises the specified instance of MyException # # @example Raise different exceptions on consecutive invocations of the expected method. # object = stub() # 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 # # @example Raise an exception on first invocation of expected method and then return values on subsequent invocations. # object = stub() # 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 # Modifies expectation so that when the expected method is called, it throws the specified +tag+ with the specific return value +object+ i.e. calls +Kernel#throw(tag, object)+. # # @param [Symbol,String] tag tag to throw to transfer control to the active catch block. # @param [Object] object return value for the catch block. # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @see Kernel#throw # @see #then # # @overload def throw(tag) # @overload def throw(tag, object) # # @example Throw tag when expected method is invoked. # object = stub() # object.stubs(:expected_method).throws(:done) # object.expected_method # => throws tag :done # # @example Throw tag with return value +object+ c.f. +Kernel#throw+. # object = stub() # object.stubs(:expected_method).throws(:done, 'result') # object.expected_method # => throws tag :done and causes catch block to return 'result' # # @example Throw different tags on consecutive invocations of the expected method. # object = stub() # object.stubs(:expected_method).throws(:done).then.throws(:continue) # object.expected_method # => throws :done # object.expected_method # => throws :continue # # @example Throw tag on first invocation of expected method and then return values for subsequent invocations. # object = stub() # object.stubs(:expected_method).throws(:done).then.returns(2, 3) # object.expected_method # => throws :done # object.expected_method # => 2 # object.expected_method # => 3 def throws(tag, object = nil) @return_values += ReturnValues.new(Thrower.new(tag, object)) self end # @overload def then # Used as syntactic sugar to improve readability. It has no effect on state of the expectation. # @overload def then(state) # Used to change the +state_machine+ to the specified state when the expected invocation occurs. # @param [StateMachine::State] state state_machine.is(state_name) provides a mechanism to change the +state_machine+ into the state specified by +state_name+ when the expected method is invoked. # # @see API#states # @see StateMachine # @see #when # # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @example Using {#then} as syntactic sugar when specifying values to be returned and exceptions to be raised on consecutive invocations of the expected method. # 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 # # @example Using {#then} to change the +state+ of a +state_machine+ on the invocation of an expected method. # 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(state = nil) add_side_effect(ChangeStateSideEffect.new(state)) if state self end # Constrains the expectation to occur only when the +state_machine+ is in the state specified by +state_predicate+. # # @param [StateMachine::StatePredicate] state_predicate +state_machine.is(state_name)+ provides a mechanism to determine whether the +state_machine+ is in the state specified by +state_predicate+ when the expected method is invoked. # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @see API#states # @see StateMachine # @see #then # # @example Using {#when} to only allow invocation of methods when "power" state machine is in the "on" state. # 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 # Constrains the 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+. # # An expected method can appear in multiple sequences. # # @param [Sequence] sequence sequence in which expected method should appear. # @param [*Array] sequences more sequences in which expected method should appear. # @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained. # # @see API#sequence # # @example Ensure methods are invoked in a specified order. # 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(sequence, *sequences) sequences.unshift(sequence).each { |seq| add_in_sequence_ordering_constraint(seq) } self end # @private attr_reader :backtrace # @private def initialize(mock, expected_method_name, backtrace = nil) @mock = mock @method_matcher = MethodMatcher.new(expected_method_name.to_sym) @parameters_matcher = ParametersMatcher.new @block_matcher = BlockMatchers::OptionalBlock.new @ordering_constraints = [] @side_effects = [] @cardinality = Cardinality.new.exactly(1) @return_values = ReturnValues.new @yield_parameters = YieldParameters.new @backtrace = backtrace || caller end # @private def add_ordering_constraint(ordering_constraint) @ordering_constraints << ordering_constraint end # @private def add_in_sequence_ordering_constraint(sequence) sequence.constrain_as_next_in_sequence(self) end # @private def add_side_effect(side_effect) @side_effects << side_effect end # @private def perform_side_effects @side_effects.each(&:perform) end # @private def in_correct_order? @ordering_constraints.all?(&:allows_invocation_now?) end # @private def matches_method?(method_name) @method_matcher.match?(method_name) end # @private def match?(invocation) @method_matcher.match?(invocation.method_name) && @parameters_matcher.match?(invocation.arguments) && @block_matcher.match?(invocation.block) && in_correct_order? end # @private def invocations_allowed? @cardinality.invocations_allowed? end # @private def satisfied? @cardinality.satisfied? end # @private def invoke(invocation) perform_side_effects @cardinality << invocation invocation.call(@yield_parameters, @return_values) end # @private def verified?(assertion_counter = nil) assertion_counter.increment if assertion_counter && @cardinality.needs_verifying? @cardinality.verified? end # @private def used? @cardinality.used? end # @private def inspect address = __id__ * 2 address += 0x100000000 if address < 0 "#" end # @private def mocha_inspect message = "#{@cardinality.anticipated_times}, #{@cardinality.invoked_times}: #{method_signature}" message << "; #{@ordering_constraints.map(&:mocha_inspect).join('; ')}" unless @ordering_constraints.empty? if Mocha.configuration.display_matching_invocations_on_failure? message << @cardinality.actual_invocations end message end # @private def method_signature signature = "#{@mock.mocha_inspect}.#{@method_matcher.mocha_inspect}#{@parameters_matcher.mocha_inspect}" signature << " #{@block_matcher.mocha_inspect}" if @block_matcher.mocha_inspect signature end # @private def definition_location filter = BacktraceFilter.new filter.filtered(backtrace)[0] end end end