Skip to content

Commit

Permalink
Display matching invocations alongside expectations
Browse files Browse the repository at this point in the history
First bunch of commits from #394.

Co-authored-by: Nitish Rathi <[email protected]>
  • Loading branch information
floehopper and nitishr committed Nov 8, 2019
2 parents 8536868 + 6ef5202 commit 00f0540
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 16 deletions.
38 changes: 24 additions & 14 deletions lib/mocha/expectation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require 'mocha/in_state_ordering_constraint'
require 'mocha/change_state_side_effect'
require 'mocha/cardinality'
require 'mocha/invocation'

module Mocha
# Methods on expectations returned from {Mock#expects}, {Mock#stubs}, {ObjectMethods#expects} and {ObjectMethods#stubs}.
Expand Down Expand Up @@ -513,10 +514,10 @@ def initialize(mock, expected_method_name, backtrace = nil)
@ordering_constraints = []
@side_effects = []
@cardinality = Cardinality.exactly(1)
@invocation_count = 0
@return_values = ReturnValues.new
@yield_parameters = YieldParameters.new
@backtrace = backtrace || caller
@invocations = []
end

# @private
Expand Down Expand Up @@ -556,33 +557,31 @@ def match?(actual_method_name, *actual_parameters)

# @private
def invocations_allowed?
@cardinality.invocations_allowed?(@invocation_count)
@cardinality.invocations_allowed?(@invocations.size)
end

# @private
def satisfied?
@cardinality.satisfied?(@invocation_count)
@cardinality.satisfied?(@invocations.size)
end

# @private
def invoke
@invocation_count += 1
def invoke(*arguments)
perform_side_effects
@yield_parameters.next_invocation.each do |yield_parameters|
yield(*yield_parameters)
end
@return_values.next
invocation = Invocation.new(method_name, @yield_parameters, @return_values)
@invocations << invocation
invocation.call(*arguments) { |*yield_args| yield(*yield_args) }
end

# @private
def verified?(assertion_counter = nil)
assertion_counter.increment if assertion_counter && @cardinality.needs_verifying?
@cardinality.verified?(@invocation_count)
@cardinality.verified?(@invocations.size)
end

# @private
def used?
@cardinality.used?(@invocation_count)
@cardinality.used?(@invocations.size)
end

# @private
Expand All @@ -595,21 +594,32 @@ def inspect
# @private
def mocha_inspect
message = "#{@cardinality.mocha_inspect}, "
message << case @invocation_count
message << case @invocations.size
when 0 then 'not yet invoked'
when 1 then 'invoked once'
when 2 then 'invoked twice'
else "invoked #{@invocation_count} times"
else "invoked #{@invocations.size} times"
end
message << ': '
message << method_signature
message << "; #{@ordering_constraints.map(&:mocha_inspect).join('; ')}" unless @ordering_constraints.empty?
message << invocations if (ENV['MOCHA_OPTIONS'] || '').split(',').include?('verbose')
message
end

# @private
def method_signature
"#{@mock.mocha_inspect}.#{@method_matcher.mocha_inspect}#{@parameters_matcher.mocha_inspect}"
"#{method_name}#{@parameters_matcher.mocha_inspect}"
end

private

def method_name
"#{@mock.mocha_inspect}.#{@method_matcher.mocha_inspect}"
end

def invocations
@invocations.map(&:mocha_inspect).join
end
end
end
31 changes: 31 additions & 0 deletions lib/mocha/invocation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require 'mocha/parameters_matcher'
require 'mocha/raised_exception'

module Mocha
class Invocation
def initialize(method_name, yield_parameters, return_values)
@method_name = method_name
@yield_parameters = yield_parameters
@return_values = return_values
@yields = []
end

def call(*arguments)
@arguments = ParametersMatcher.new(arguments)
@yield_parameters.next_invocation.each do |yield_parameters|
@yields << ParametersMatcher.new(yield_parameters)
yield(*yield_parameters)
end
@result = @return_values.next
rescue Exception => e # rubocop:disable Lint/RescueException
@result = RaisedException.new(e)
raise
end

def mocha_inspect
desc = "\n - #{@method_name}#{@arguments.mocha_inspect} # => #{@result.mocha_inspect}"
desc << " after yielding #{@yields.map(&:mocha_inspect).join(', then ')}" if @yields.any?
desc
end
end
end
4 changes: 2 additions & 2 deletions lib/mocha/mock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -315,11 +315,11 @@ def method_missing(symbol, *arguments, &block)
raise NoMethodError, "undefined method `#{symbol}' for #{mocha_inspect} which responds like #{@responder.mocha_inspect}"
end
if (matching_expectation_allowing_invocation = all_expectations.match_allowing_invocation(symbol, *arguments))
matching_expectation_allowing_invocation.invoke(&block)
matching_expectation_allowing_invocation.invoke(*arguments, &block)
elsif (matching_expectation = all_expectations.match(symbol, *arguments)) || (!matching_expectation && !@everything_stubbed)
if @unexpected_invocation.nil?
@unexpected_invocation = UnexpectedInvocation.new(self, symbol, *arguments)
matching_expectation.invoke(&block) if matching_expectation
matching_expectation.invoke(*arguments, &block) if matching_expectation
message = @unexpected_invocation.full_description
message << @mockery.mocha_inspect
else
Expand Down
11 changes: 11 additions & 0 deletions lib/mocha/raised_exception.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Mocha
class RaisedException
def initialize(exception)
@exception = exception
end

def mocha_inspect
"raised #{@exception}"
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
require File.expand_path('../acceptance_test_helper', __FILE__)
require 'mocha/setup'

class DisplayMatchingInvocationsAlongsideExpectationsTest < Mocha::TestCase
include AcceptanceTest

def setup
setup_acceptance_test
@original_env = ENV.to_hash
ENV['MOCHA_OPTIONS'] = 'verbose'
end

def teardown
ENV.replace(@original_env)
teardown_acceptance_test
end

def test_should_display_results
test_result = run_as_test do
foo = mock('foo')
foo.expects(:bar).with(1).returns('a')
foo.stubs(:bar).with(any_parameters).returns('f').raises(StandardError)

foo.bar(1, 2)
assert_raise(StandardError) { foo.bar(3, 4) }
end
assert_invocations(
test_result,
'- allowed any number of times, invoked twice: #<Mock:foo>.bar(any_parameters)',
' - #<Mock:foo>.bar(1, 2) # => "f"',
' - #<Mock:foo>.bar(3, 4) # => raised StandardError'
)
end

def test_should_display_yields
test_result = run_as_test do
foo = mock('foo')
foo.expects(:bar).with(1).returns('a')
foo.stubs(:bar).with(any_parameters).multiple_yields(%w[b c], %w[d e]).returns('f').raises(StandardError)

foo.bar(1, 2) { |_ignored| }
assert_raise(StandardError) { foo.bar(3, 4) { |_ignored| } }
end
assert_invocations(
test_result,
'- allowed any number of times, invoked twice: #<Mock:foo>.bar(any_parameters)',
' - #<Mock:foo>.bar(1, 2) # => "f" after yielding ("b", "c"), then ("d", "e")',
' - #<Mock:foo>.bar(3, 4) # => raised StandardError after yielding ("b", "c"), then ("d", "e")'
)
end

def test_should_display_empty_yield_and_return
test_result = run_as_test do
foo = mock('foo')
foo.expects(:bar).with(1).returns('a')
foo.stubs(:bar).with(any_parameters).yields

foo.bar(1, 2) { |_ignored| }
end
assert_invocations(
test_result,
'- allowed any number of times, invoked once: #<Mock:foo>.bar(any_parameters)',
' - #<Mock:foo>.bar(1, 2) # => nil after yielding ()'
)
end

def assert_invocations(test_result, *invocations)
assert_failed(test_result)
assert_equal invocations.unshift('satisfied expectations:'),
test_result.failure_message_lines[-invocations.size..-1]
end
end

0 comments on commit 00f0540

Please sign in to comment.