diff --git a/lib/mocha/expectation.rb b/lib/mocha/expectation.rb index 63a46becb..e8653e7d2 100644 --- a/lib/mocha/expectation.rb +++ b/lib/mocha/expectation.rb @@ -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}. @@ -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 @@ -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 @@ -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 diff --git a/lib/mocha/invocation.rb b/lib/mocha/invocation.rb new file mode 100644 index 000000000..03568a58b --- /dev/null +++ b/lib/mocha/invocation.rb @@ -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 diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index e917cd65b..6fb0fbb52 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -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 diff --git a/lib/mocha/raised_exception.rb b/lib/mocha/raised_exception.rb new file mode 100644 index 000000000..8a1ad9720 --- /dev/null +++ b/lib/mocha/raised_exception.rb @@ -0,0 +1,11 @@ +module Mocha + class RaisedException + def initialize(exception) + @exception = exception + end + + def mocha_inspect + "raised #{@exception}" + end + end +end diff --git a/test/acceptance/display_matching_invocations_alongside_expectations_test.rb b/test/acceptance/display_matching_invocations_alongside_expectations_test.rb new file mode 100644 index 000000000..236da47ed --- /dev/null +++ b/test/acceptance/display_matching_invocations_alongside_expectations_test.rb @@ -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: #.bar(any_parameters)', + ' - #.bar(1, 2) # => "f"', + ' - #.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: #.bar(any_parameters)', + ' - #.bar(1, 2) # => "f" after yielding ("b", "c"), then ("d", "e")', + ' - #.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: #.bar(any_parameters)', + ' - #.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