diff --git a/README.md b/README.md index c63cd508..0ee35884 100644 --- a/README.md +++ b/README.md @@ -276,7 +276,7 @@ Maybe, but probably not. Partial mocking changes the state of objects in the `Ob Stubs and expectations are basically the same thing. A stub is just an expectation of zero or more invocations. The `Expectation#stubs` method is syntactic sugar to make the intent of the test more explicit. -When a method is invoked on a mock object, the mock object searches through its expectations from newest to oldest to find one that matches the invocation. After the invocation, the matching expectation might stop matching further invocations. +When a method is invoked on a mock object, the mock object searches through its expectations from newest to oldest to find one that matches the invocation. After the invocation, the matching expectation might stop matching further invocations. If the expectation that matches the invocation has a cardinality of "never", then an unexpected invocation error is reported. See the [documentation](https://mocha.jamesmead.org/Mocha/Mock.html) for `Mocha::Mock` for further details. diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index 7a5ef92e..645116ef 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -35,6 +35,9 @@ module Mocha # while an +expects(:foo).at_least_once+ expectation will always be matched # against invocations. # + # However, note that if the expectation that matches the invocation has a + # cardinality of "never", then an unexpected invocation error is reported. + # # This scheme allows you to: # # - Set up default stubs in your the +setup+ method of your test class and @@ -318,25 +321,27 @@ def method_missing(symbol, *arguments, &block) # rubocop:disable Style/MethodMis ruby2_keywords(:method_missing) # @private - def handle_method_call(symbol, arguments, block) + def handle_method_call(symbol, arguments, block) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity check_expiry check_responder_responds_to(symbol) invocation = Invocation.new(self, symbol, arguments, block) matching_expectations = all_expectations.matching_expectations(invocation) - matching_expectation_allowing_invocation = matching_expectations.detect(&:invocations_allowed?) - matching_expectation_never_allowing_invocation = matching_expectations.detect(&:invocations_never_allowed?) - if matching_expectation_allowing_invocation - if matching_expectation_never_allowing_invocation - invocation_not_allowed_warning(invocation, matching_expectation_never_allowing_invocation) - end - matching_expectation_allowing_invocation.invoke(invocation) - else - matching_expectation_ignoring_order = all_expectations.match(invocation, ignoring_order: true) - if matching_expectation_ignoring_order || (!matching_expectation_ignoring_order && !@everything_stubbed) - raise_unexpected_invocation_error(invocation, matching_expectation_ignoring_order) + index = 0 + while index < matching_expectations.length + matching_expectation = matching_expectations[index] + if matching_expectation.invocations_never_allowed? + raise_unexpected_invocation_error(invocation, matching_expectation) + elsif matching_expectation.invocations_allowed? + return matching_expectation.invoke(invocation) end + index += 1 + end + + matching_expectation_ignoring_order = all_expectations.match(invocation, ignoring_order: true) + if matching_expectation_ignoring_order || (!matching_expectation_ignoring_order && !@everything_stubbed) # rubocop:disable Style/GuardClause + raise_unexpected_invocation_error(invocation, matching_expectation_ignoring_order) end end diff --git a/test/acceptance/mocked_methods_dispatch_test.rb b/test/acceptance/mocked_methods_dispatch_test.rb index 168cfe40..af89da12 100644 --- a/test/acceptance/mocked_methods_dispatch_test.rb +++ b/test/acceptance/mocked_methods_dispatch_test.rb @@ -1,6 +1,4 @@ require File.expand_path('../acceptance_test_helper', __FILE__) -require 'deprecation_disabler' -require 'execution_point' class MockedMethodDispatchTest < Mocha::TestCase include AcceptanceTest @@ -75,23 +73,30 @@ def test_should_find_latest_expectation_with_range_of_expected_invocation_count_ assert_passed(test_result) end - def test_should_display_deprecation_warning_if_invocation_matches_expectation_with_never_cardinality - execution_point = nil + def test_should_fail_fast_if_invocation_matches_expectation_with_never_cardinality test_result = run_as_test do mock = mock('mock') mock.stubs(:method) - mock.expects(:method).never; execution_point = ExecutionPoint.current - DeprecationDisabler.disable_deprecations do - mock.method - end + mock.expects(:method).never + mock.method + end + assert_failed(test_result) + assert_equal [ + 'unexpected invocation: #.method()', + 'unsatisfied expectations:', + '- expected never, invoked once: #.method(any_parameters)', + 'satisfied expectations:', + '- allowed any number of times, invoked never: #.method(any_parameters)' + ], test_result.failure_message_lines + end + + def test_should_not_fail_fast_if_invocation_matches_expectation_allowing_invocation_before_matching_expectation_with_never_cardinality + test_result = run_as_test do + mock = mock('mock') + mock.expects(:method).never + mock.expects(:method).once + mock.method end assert_passed(test_result) - message = Mocha::Deprecation.messages.last - location = execution_point.location - expected = [ - "The expectation defined at #{location} does not allow invocations, but #.method() was invoked.", - 'This invocation will cause the test to fail fast in a future version of Mocha.' - ] - assert_equal expected.join(' '), message end end