rspec/rspec-mocks

View on GitHub
lib/rspec/mocks/message_expectation.rb

Summary

Maintainability
D
2 days
Test Coverage
RSpec::Support.require_rspec_support 'mutex'

module RSpec
  module Mocks
    # A message expectation that only allows concrete return values to be set
    # for a message. While this same effect can be achieved using a standard
    # MessageExpectation, this version is much faster and so can be used as an
    # optimization.
    #
    # @private
    class SimpleMessageExpectation
      def initialize(message, response, error_generator, backtrace_line=nil)
        @message, @response, @error_generator, @backtrace_line = message.to_sym, response, error_generator, backtrace_line
        @received = false
      end

      def invoke(*_)
        @received = true
        @response
      end

      def matches?(message, *_)
        @message == message.to_sym
      end

      def called_max_times?
        false
      end

      def verify_messages_received
        return if @received
        @error_generator.raise_expectation_error(
          @message, 1, ArgumentListMatcher::MATCH_ALL, 0, nil, [], @backtrace_line
        )
      end

      def unadvise(_)
      end
    end

    # Represents an individual method stub or message expectation. The methods
    # defined here can be used to configure how it behaves. The methods return
    # `self` so that they can be chained together to form a fluent interface.
    class MessageExpectation
      # @!group Configuring Responses

      # @overload and_return(value)
      # @overload and_return(first_value, second_value)
      #
      # 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.
      #
      # @return [nil] No further chaining is supported after this.
      # @example
      #   allow(counter).to receive(:count).and_return(1)
      #   counter.count # => 1
      #   counter.count # => 1
      #
      #   allow(counter).to receive(:count).and_return(1,2,3)
      #   counter.count # => 1
      #   counter.count # => 2
      #   counter.count # => 3
      #   counter.count # => 3
      #   counter.count # => 3
      #   # etc
      def and_return(first_value, *values)
        raise_already_invoked_error_if_necessary(__method__)
        if negative?
          raise "`and_return` is not supported with negative message expectations"
        end

        if block_given?
          raise ArgumentError, "Implementation blocks aren't supported with `and_return`"
        end

        values.unshift(first_value)
        @expected_received_count = [@expected_received_count, values.size].max unless ignoring_args? || (@expected_received_count == 0 && @at_least)
        self.terminal_implementation_action = AndReturnImplementation.new(values)

        nil
      end

      # Tells the object to delegate to the original unmodified method
      # when it receives the message.
      #
      # @note This is only available on partial doubles.
      #
      # @return [nil] No further chaining is supported after this.
      # @example
      #   expect(counter).to receive(:increment).and_call_original
      #   original_count = counter.count
      #   counter.increment
      #   expect(counter.count).to eq(original_count + 1)
      def and_call_original
        wrap_original(__method__) do |original, *args, &block|
          original.call(*args, &block)
        end
      end

      # Decorates the stubbed method with the supplied block. The original
      # unmodified method is passed to the block along with any method call
      # arguments so you can delegate to it, whilst still being able to
      # change what args are passed to it and/or change the return value.
      #
      # @note This is only available on partial doubles.
      #
      # @return [nil] No further chaining is supported after this.
      # @example
      #   expect(api).to receive(:large_list).and_wrap_original do |original_method, *args, &block|
      #     original_method.call(*args, &block).first(10)
      #   end
      def and_wrap_original(&block)
        wrap_original(__method__, &block)
      end

      # @overload and_raise
      # @overload and_raise(ExceptionClass)
      # @overload and_raise(ExceptionClass, message)
      # @overload and_raise(exception_instance)
      #
      # Tells the object to raise an exception when the message is received.
      #
      # @return [nil] No further chaining is supported after this.
      # @note
      #   When you pass an exception class, the MessageExpectation will raise
      #   an instance of it, creating it with `exception` and passing `message`
      #   if specified.  If the exception class initializer requires more than
      #   one parameters, you must pass in an instance and not the class,
      #   otherwise this method will raise an ArgumentError exception.
      #
      # @example
      #   allow(car).to receive(:go).and_raise
      #   allow(car).to receive(:go).and_raise(OutOfGas)
      #   allow(car).to receive(:go).and_raise(OutOfGas, "At least 2 oz of gas needed to drive")
      #   allow(car).to receive(:go).and_raise(OutOfGas.new(2, :oz))
      def and_raise(*args)
        raise_already_invoked_error_if_necessary(__method__)
        self.terminal_implementation_action = Proc.new { raise(*args) }
        nil
      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.
      #
      # @return [nil] No further chaining is supported after this.
      # @example
      #   allow(car).to receive(:go).and_throw(:out_of_gas)
      #   allow(car).to receive(:go).and_throw(:out_of_gas, :level => 0.1)
      def and_throw(*args)
        raise_already_invoked_error_if_necessary(__method__)
        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
      # is received.
      #
      # @return [MessageExpectation] self, to support further chaining.
      # @example
      #   stream.stub(:open).and_yield(StringIO.new)
      def and_yield(*args, &block)
        raise_already_invoked_error_if_necessary(__method__)
        yield @eval_context = Object.new if block

        # Initialize args to yield now that it's being used, see also: comment
        # in constructor.
        @args_to_yield ||= []

        @args_to_yield << args
        self.initial_implementation_action = AndYieldImplementation.new(@args_to_yield, @eval_context, @error_generator)
        self
      end
      # @!endgroup

      # @!group Constraining Receive Counts

      # Constrain a message expectation to be received a specific number of
      # times.
      #
      # @return [MessageExpectation] self, to support further chaining.
      # @example
      #   expect(dealer).to receive(:deal_card).exactly(10).times
      def exactly(n, &block)
        raise_already_invoked_error_if_necessary(__method__)
        self.inner_implementation_action = block
        set_expected_received_count :exactly, n
        self
      end

      # Constrain a message expectation to be received at least a specific
      # number of times.
      #
      # @return [MessageExpectation] self, to support further chaining.
      # @example
      #   expect(dealer).to receive(:deal_card).at_least(9).times
      def at_least(n, &block)
        raise_already_invoked_error_if_necessary(__method__)
        set_expected_received_count :at_least, n

        if n == 0
          raise "at_least(0) has been removed, use allow(...).to receive(:message) instead"
        end

        self.inner_implementation_action = block

        self
      end

      # Constrain a message expectation to be received at most a specific
      # number of times.
      #
      # @return [MessageExpectation] self, to support further chaining.
      # @example
      #   expect(dealer).to receive(:deal_card).at_most(10).times
      def at_most(n, &block)
        raise_already_invoked_error_if_necessary(__method__)
        self.inner_implementation_action = block
        set_expected_received_count :at_most, n
        self
      end

      # Syntactic sugar for `exactly`, `at_least` and `at_most`
      #
      # @return [MessageExpectation] self, to support further chaining.
      # @example
      #   expect(dealer).to receive(:deal_card).exactly(10).times
      #   expect(dealer).to receive(:deal_card).at_least(10).times
      #   expect(dealer).to receive(:deal_card).at_most(10).times
      def times(&block)
        self.inner_implementation_action = block
        self
      end
      alias time times

      # Expect a message not to be received at all.
      #
      # @return [MessageExpectation] self, to support further chaining.
      # @example
      #   expect(car).to receive(:stop).never
      def never
        error_generator.raise_double_negation_error("expect(obj)") if negative?
        @expected_received_count = 0
        self
      end

      # Expect a message to be received exactly one time.
      #
      # @return [MessageExpectation] self, to support further chaining.
      # @example
      #   expect(car).to receive(:go).once
      def once(&block)
        self.inner_implementation_action = block
        set_expected_received_count :exactly, 1
        self
      end

      # Expect a message to be received exactly two times.
      #
      # @return [MessageExpectation] self, to support further chaining.
      # @example
      #   expect(car).to receive(:go).twice
      def twice(&block)
        self.inner_implementation_action = block
        set_expected_received_count :exactly, 2
        self
      end

      # Expect a message to be received exactly three times.
      #
      # @return [MessageExpectation] self, to support further chaining.
      # @example
      #   expect(car).to receive(:go).thrice
      def thrice(&block)
        self.inner_implementation_action = block
        set_expected_received_count :exactly, 3
        self
      end
      # @!endgroup

      # @!group Other Constraints

      # 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.
      #
      # @return [MessageExpectation] self, to support further chaining.
      # @example
      #   allow(cart).to receive(:add) { :failure }
      #   allow(cart).to receive(:add).with(Book.new(:isbn => 1934356379)) { :success }
      #   cart.add(Book.new(:isbn => 1234567890))
      #   # => :failure
      #   cart.add(Book.new(:isbn => 1934356379))
      #   # => :success
      #
      #   expect(cart).to 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)
        raise_already_invoked_error_if_necessary(__method__)
        if args.empty?
          raise ArgumentError,
                "`with` must have at least one argument. Use `no_args` matcher to set the expectation of receiving no arguments."
        end

        self.inner_implementation_action = block
        @argument_list_matcher = ArgumentListMatcher.new(*args)
        self
      end

      # Expect messages to be received in a specific order.
      #
      # @return [MessageExpectation] self, to support further chaining.
      # @example
      #   expect(api).to receive(:prepare).ordered
      #   expect(api).to receive(:run).ordered
      #   expect(api).to receive(:finish).ordered
      def ordered(&block)
        if type == :stub
          RSpec.warning(
            "`allow(...).to receive(..).ordered` is not supported and will " \
            "have no effect, use `and_return(*ordered_values)` instead."
          )
        end

        self.inner_implementation_action = block
        additional_expected_calls.times do
          @order_group.register(self)
        end
        @ordered = true
        self
      end

      # @return [String] a nice representation of the message expectation
      def to_s
        args_description = error_generator.method_call_args_description(@argument_list_matcher.expected_args, "", "") { true }
        args_description = "(#{args_description})" unless args_description.start_with?("(")
        "#<#{self.class} #{error_generator.intro}.#{message}#{args_description}>"
      end
      alias inspect to_s

      # @private
      # Contains the parts of `MessageExpectation` that aren't part of
      # rspec-mocks' public API. The class is very big and could really use
      # some collaborators it delegates to for this stuff but for now this was
      # the simplest way to split the public from private stuff to make it
      # easier to publish the docs for the APIs we want published.
      # rubocop:disable Metrics/ModuleLength
      module ImplementationDetails
        attr_accessor :error_generator, :implementation
        attr_reader :message
        attr_reader :orig_object
        attr_writer :expected_received_count, :expected_from, :argument_list_matcher
        protected :expected_received_count=, :expected_from=, :error_generator=, :implementation=

        # @private
        attr_reader :type

        # rubocop:disable Metrics/ParameterLists
        def initialize(error_generator, expectation_ordering, expected_from, method_double,
                       type=:expectation, opts={}, &implementation_block)
          @type = type
          @error_generator = error_generator
          @error_generator.opts = error_generator.opts.merge(opts)
          @expected_from = expected_from
          @method_double = method_double
          @orig_object = @method_double.object
          @message = @method_double.method_name
          @actual_received_count = 0
          @actual_received_count_write_mutex = Support::Mutex.new
          @expected_received_count = type == :expectation ? 1 : :any
          @argument_list_matcher = ArgumentListMatcher::MATCH_ALL
          @order_group = expectation_ordering
          @order_group.register(self) unless type == :stub
          @expectation_type = type
          @ordered = false
          @at_least = @at_most = @exactly = nil

          # Initialized to nil so that we don't allocate an array for every
          # mock or stub. See also comment in `and_yield`.
          @args_to_yield = nil
          @eval_context = nil
          @yield_receiver_to_implementation_block = false

          @implementation = Implementation.new
          self.inner_implementation_action = implementation_block
        end
        # rubocop:enable Metrics/ParameterLists

        def expected_args
          @argument_list_matcher.expected_args
        end

        def and_yield_receiver_to_implementation
          @yield_receiver_to_implementation_block = true
          self
        end

        def yield_receiver_to_implementation_block?
          @yield_receiver_to_implementation_block
        end

        def matches?(message, *args)
          @message == message && @argument_list_matcher.args_match?(*args)
        end

        def safe_invoke(parent_stub, *args, &block)
          invoke_incrementing_actual_calls_by(1, false, parent_stub, *args, &block)
        end

        def invoke(parent_stub, *args, &block)
          invoke_incrementing_actual_calls_by(1, true, parent_stub, *args, &block)
        end

        def invoke_without_incrementing_received_count(parent_stub, *args, &block)
          invoke_incrementing_actual_calls_by(0, true, parent_stub, *args, &block)
        end

        def negative?
          @expected_received_count == 0 && !@at_least
        end

        def called_max_times?
          @expected_received_count != :any &&
            !@at_least &&
            @expected_received_count > 0 &&
            @actual_received_count >= @expected_received_count
        end

        def matches_name_but_not_args(message, *args)
          @message == message && !@argument_list_matcher.args_match?(*args)
        end

        def verify_messages_received
          return if expected_messages_received?
          generate_error
        end

        def expected_messages_received?
          ignoring_args? || matches_exact_count? || matches_at_least_count? || matches_at_most_count?
        end

        def ensure_expected_ordering_received!
          @order_group.verify_invocation_order(self) if @ordered
          true
        end

        def ignoring_args?
          @expected_received_count == :any
        end

        def matches_at_least_count?
          @at_least && @actual_received_count >= @expected_received_count
        end

        def matches_at_most_count?
          @at_most && @actual_received_count <= @expected_received_count
        end

        def matches_exact_count?
          @expected_received_count == @actual_received_count
        end

        def similar_messages
          @similar_messages ||= []
        end

        def advise(*args)
          similar_messages << args
        end

        def unadvise(args)
          similar_messages.delete_if { |message| args.include?(message) }
        end

        def generate_error
          if similar_messages.empty?
            @error_generator.raise_expectation_error(
              @message, @expected_received_count, @argument_list_matcher,
              @actual_received_count, expectation_count_type, expected_args,
              @expected_from, exception_source_id
            )
          else
            @error_generator.raise_similar_message_args_error(
              self, @similar_messages, @expected_from
            )
          end
        end

        def raise_unexpected_message_args_error(args_for_multiple_calls)
          @error_generator.raise_unexpected_message_args_error(self, args_for_multiple_calls, exception_source_id)
        end

        def expectation_count_type
          return :at_least if @at_least
          return :at_most if @at_most
          nil
        end

        def description_for(verb)
          @error_generator.describe_expectation(
            verb, @message, @expected_received_count,
            @actual_received_count, expected_args
          )
        end

        def raise_out_of_order_error
          @error_generator.raise_out_of_order_error @message
        end

        def additional_expected_calls
          return 0 if @expectation_type == :stub || !@exactly
          @expected_received_count - 1
        end

        def ordered?
          @ordered
        end

        def negative_expectation_for?(message)
          @message == message && negative?
        end

        def actual_received_count_matters?
          @at_least || @at_most || @exactly
        end

        def increase_actual_received_count!
          @actual_received_count_write_mutex.synchronize do
            @actual_received_count += 1
          end
        end

      private

        def exception_source_id
          @exception_source_id ||= "#{self.class.name} #{__id__}"
        end

        def invoke_incrementing_actual_calls_by(increment, allowed_to_fail, parent_stub, *args, &block)
          args.unshift(orig_object) if yield_receiver_to_implementation_block?

          if negative? || (allowed_to_fail && (@exactly || @at_most) && (@actual_received_count == @expected_received_count))
            # args are the args we actually received, @argument_list_matcher is the
            # list of args we were expecting
            @error_generator.raise_expectation_error(
              @message, @expected_received_count,
              @argument_list_matcher,
              @actual_received_count + increment,
              expectation_count_type, args, nil, exception_source_id
            )
          end

          @order_group.handle_order_constraint self

          if implementation.present?
            implementation.call(*args, &block)
          elsif parent_stub
            parent_stub.invoke(nil, *args, &block)
          end
        ensure
          @actual_received_count_write_mutex.synchronize do
            @actual_received_count += increment
          end
        end

        def has_been_invoked?
          @actual_received_count > 0
        end

        def raise_already_invoked_error_if_necessary(calling_customization)
          return unless has_been_invoked?

          error_generator.raise_already_invoked_error(message, calling_customization)
        end

        def set_expected_received_count(relativity, n)
          raise "`count` is not supported with negative message expectations" if negative?
          @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
                                     when :thrice then 3
                                     end
        end

        def initial_implementation_action=(action)
          implementation.initial_action = action
        end

        def inner_implementation_action=(action)
          return unless action
          warn_about_stub_override if implementation.inner_action
          implementation.inner_action = action
        end

        def terminal_implementation_action=(action)
          implementation.terminal_action = action
        end

        def warn_about_stub_override
          RSpec.warning(
            "You're overriding a previous stub implementation of `#{@message}`. " \
            "Called from #{CallerFilter.first_non_rspec_line}."
          )
        end

        def wrap_original(method_name, &block)
          if RSpec::Mocks::TestDouble === @method_double.object
            @error_generator.raise_only_valid_on_a_partial_double(method_name)
          else
            warn_about_stub_override if implementation.inner_action
            @implementation = AndWrapOriginalImplementation.new(@method_double.original_implementation_callable, block)
            @yield_receiver_to_implementation_block = false
          end

          nil
        end
      end
      # rubocop:enable Metrics/ModuleLength

      include ImplementationDetails
    end

    # Handles the implementation of an `and_yield` declaration.
    # @private
    class AndYieldImplementation
      def initialize(args_to_yield, eval_context, error_generator)
        @args_to_yield = args_to_yield
        @eval_context = eval_context
        @error_generator = error_generator
      end

      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
        value = nil
        block_signature = Support::BlockSignature.new(block)

        @args_to_yield.each do |args|
          unless Support::StrictSignatureVerifier.new(block_signature, args).valid?
            @error_generator.raise_wrong_arity_error(args, block_signature)
          end

          value = @eval_context ? @eval_context.instance_exec(*args, &block) : yield(*args)
        end
        value
      end
    end

    # Handles the implementation of an `and_return` implementation.
    # @private
    class AndReturnImplementation
      def initialize(values_to_return)
        @values_to_return = values_to_return
      end

      def call(*_args_to_ignore, &_block)
        if @values_to_return.size > 1
          @values_to_return.shift
        else
          @values_to_return.first
        end
      end
    end

    # Represents a configured implementation. Takes into account
    # any number of sub-implementations.
    # @private
    class Implementation
      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 AndWrapOriginalImplementation
      def initialize(method, block)
        @method = method
        @block = block
      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 inner_action
        true
      end

      def call(*args, &block)
        @block.call(@method, *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