rspec/rspec-expectations

View on GitHub
lib/rspec/expectations/expectation_target.rb

Summary

Maintainability
A
25 mins
Test Coverage
module RSpec
  module Expectations
    # Wraps the target of an expectation.
    #
    # @example
    #   expect(something)       # => ExpectationTarget wrapping something
    #   expect { do_something } # => ExpectationTarget wrapping the block
    #
    #   # used with `to`
    #   expect(actual).to eq(3)
    #
    #   # with `not_to`
    #   expect(actual).not_to eq(3)
    #
    # @note `ExpectationTarget` is not intended to be instantiated
    #   directly by users. Use `expect` instead.
    class ExpectationTarget
      # @private
      # Used as a sentinel value to be able to tell when the user
      # did not pass an argument. We can't use `nil` for that because
      # `nil` is a valid value to pass.
      UndefinedValue = Module.new

      # @note this name aligns with `Minitest::Expectation` so that our
      #   {InstanceMethods} module can be included in that class when
      #   used in a Minitest context.
      # @return [Object] the target of the expectation
      attr_reader :target

      # @api private
      def initialize(value)
        @target = value
      end

      # @private
      def self.for(value, block)
        if UndefinedValue.equal?(value)
          unless block
            raise ArgumentError, "You must pass either an argument or a block to `expect`."
          end
          BlockExpectationTarget.new(block)
        elsif block
          raise ArgumentError, "You cannot pass both an argument and a block to `expect`."
        else
          new(value)
        end
      end

      # Defines instance {ExpectationTarget} instance methods. These are defined
      # in a module so we can include it in `Minitest::Expectation` when
      # `rspec/expectations/minitest_integration` is loaded in order to
      # support usage with Minitest.
      module InstanceMethods
        # Runs the given expectation, passing if `matcher` returns true.
        # @example
        #   expect(value).to eq(5)
        #   expect { perform }.to raise_error
        # @param [Matcher]
        #   matcher
        # @param [String or Proc] message optional message to display when the expectation fails
        # @return [Boolean] true if the expectation succeeds (else raises)
        # @see RSpec::Matchers
        def to(matcher=nil, message=nil, &block)
          prevent_operator_matchers(:to) unless matcher
          RSpec::Expectations::PositiveExpectationHandler.handle_matcher(target, matcher, message, &block)
        end

        # Runs the given expectation, passing if `matcher` returns false.
        # @example
        #   expect(value).not_to eq(5)
        # @param [Matcher]
        #   matcher
        # @param [String or Proc] message optional message to display when the expectation fails
        # @return [Boolean] false if the negative expectation succeeds (else raises)
        # @see RSpec::Matchers
        def not_to(matcher=nil, message=nil, &block)
          prevent_operator_matchers(:not_to) unless matcher
          RSpec::Expectations::NegativeExpectationHandler.handle_matcher(target, matcher, message, &block)
        end
        alias to_not not_to

      private

        def prevent_operator_matchers(verb)
          raise ArgumentError, "The expect syntax does not support operator matchers, " \
                               "so you must pass a matcher to `##{verb}`."
        end
      end

      include InstanceMethods
    end

    # @private
    # Validates the provided matcher to ensure it supports block
    # expectations, in order to avoid user confusion when they
    # use a block thinking the expectation will be on the return
    # value of the block rather than the block itself.
    class BlockExpectationTarget < ExpectationTarget
      def to(matcher, message=nil, &block)
        enforce_block_expectation(matcher)
        super
      end

      def not_to(matcher, message=nil, &block)
        enforce_block_expectation(matcher)
        super
      end
      alias to_not not_to

    private

      def enforce_block_expectation(matcher)
        return if supports_block_expectations?(matcher)

        raise ExpectationNotMetError, "You must pass an argument rather than a block to `expect` to use the provided " \
          "matcher (#{RSpec::Support::ObjectFormatter.format(matcher)}), or the matcher must implement " \
          "`supports_block_expectations?`."
      end

      def supports_block_expectations?(matcher)
        matcher.supports_block_expectations?
      rescue NoMethodError
        false
      end
    end
  end
end