Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fail fast if invocation matches never expectation #679

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
29 changes: 17 additions & 12 deletions lib/mocha/mock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
35 changes: 20 additions & 15 deletions test/acceptance/mocked_methods_dispatch_test.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
require File.expand_path('../acceptance_test_helper', __FILE__)
require 'deprecation_disabler'
require 'execution_point'

class MockedMethodDispatchTest < Mocha::TestCase
include AcceptanceTest
Expand Down Expand Up @@ -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: #<Mock:mock>.method()',
'unsatisfied expectations:',
'- expected never, invoked once: #<Mock:mock>.method(any_parameters)',
'satisfied expectations:',
'- allowed any number of times, invoked never: #<Mock:mock>.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 #<Mock:mock>.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