diff --git a/lib/mocha/cardinality.rb b/lib/mocha/cardinality.rb index ab9b4f38..fa8fb99d 100644 --- a/lib/mocha/cardinality.rb +++ b/lib/mocha/cardinality.rb @@ -34,6 +34,10 @@ def invocations_allowed? @invocations.size < maximum end + def invocations_never_allowed? + maximum.zero? + end + def satisfied? @invocations.size >= required end diff --git a/lib/mocha/expectation.rb b/lib/mocha/expectation.rb index 5e6f95d4..f55918bd 100644 --- a/lib/mocha/expectation.rb +++ b/lib/mocha/expectation.rb @@ -653,6 +653,11 @@ def invocations_allowed? @cardinality.invocations_allowed? end + # @private + def invocations_never_allowed? + @cardinality.invocations_never_allowed? + end + # @private def satisfied? @cardinality.satisfied? diff --git a/lib/mocha/expectation_list.rb b/lib/mocha/expectation_list.rb index ba521b62..2bd45dc2 100644 --- a/lib/mocha/expectation_list.rb +++ b/lib/mocha/expectation_list.rb @@ -25,6 +25,10 @@ def match_allowing_invocation(invocation) matching_expectations(invocation).detect(&:invocations_allowed?) end + def match_never_allowing_invocation(invocation) + matching_expectations(invocation).detect(&:invocations_never_allowed?) + end + def verified?(assertion_counter = nil) @expectations.all? { |expectation| expectation.verified?(assertion_counter) } end @@ -49,8 +53,6 @@ def +(other) self.class.new(to_a + other.to_a) end - private - def matching_expectations(invocation, ignoring_order: false) @expectations.select { |e| e.match?(invocation, ignoring_order: ignoring_order) } end diff --git a/lib/mocha/mock.rb b/lib/mocha/mock.rb index 03c9c1c1..e44ecee4 100644 --- a/lib/mocha/mock.rb +++ b/lib/mocha/mock.rb @@ -321,10 +321,18 @@ def handle_method_call(symbol, arguments, block) check_expiry check_responder_responds_to(symbol) invocation = Invocation.new(self, symbol, arguments, block) - if (matching_expectation_allowing_invocation = all_expectations.match_allowing_invocation(invocation)) + + 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 && !matching_expectation_never_allowing_invocation matching_expectation_allowing_invocation.invoke(invocation) - elsif (matching_expectation = all_expectations.match(invocation, ignoring_order: true)) || (!matching_expectation && !@everything_stubbed) - raise_unexpected_invocation_error(invocation, matching_expectation) + 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) + end end end diff --git a/test/acceptance/mocked_methods_dispatch_test.rb b/test/acceptance/mocked_methods_dispatch_test.rb index 9b53aa99..5789dc42 100644 --- a/test/acceptance/mocked_methods_dispatch_test.rb +++ b/test/acceptance/mocked_methods_dispatch_test.rb @@ -72,4 +72,21 @@ def test_should_find_latest_expectation_with_range_of_expected_invocation_count_ end assert_passed(test_result) end + + 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 + 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 end diff --git a/test/acceptance/parameter_matcher_test.rb b/test/acceptance/parameter_matcher_test.rb index 88530151..2bbbc588 100644 --- a/test/acceptance/parameter_matcher_test.rb +++ b/test/acceptance/parameter_matcher_test.rb @@ -373,4 +373,15 @@ def test_should_not_match_parameters_when_values_do_not_add_up_to_ten end assert_failed(test_result) end + + def test_should_only_call_matcher_block_once + test_result = run_as_test do + number_of_invocations = 0 + mock = mock() + mock.stubs(:method).with { number_of_invocations += 1; true } + mock.method + assert_equal 1, number_of_invocations + end + assert_passed(test_result) + end end diff --git a/test/unit/cardinality_test.rb b/test/unit/cardinality_test.rb index 1c265e97..167d8e9a 100644 --- a/test/unit/cardinality_test.rb +++ b/test/unit/cardinality_test.rb @@ -22,6 +22,13 @@ def test_should_allow_invocations_if_invocation_count_has_not_yet_reached_maximu assert !cardinality.invocations_allowed? end + def test_should_never_allow_invocations + cardinality = Cardinality.new.exactly(0) + assert cardinality.invocations_never_allowed? + cardinality << new_invocation + assert cardinality.invocations_never_allowed? + end + def test_should_be_satisfied_if_invocations_so_far_have_reached_required_threshold cardinality = Cardinality.new(2, 3) assert !cardinality.satisfied? diff --git a/test/unit/expectation_list_test.rb b/test/unit/expectation_list_test.rb index e8c36f88..a0c345c5 100644 --- a/test/unit/expectation_list_test.rb +++ b/test/unit/expectation_list_test.rb @@ -69,6 +69,17 @@ def test_should_find_most_recent_matching_expectation_allowing_invocation assert_same expectation1, expectation_list.match_allowing_invocation(Invocation.new(:irrelevant, :my_method)) end + def test_should_find_matching_expectation_never_allowing_invocation + expectation_list = ExpectationList.new + expectation1 = Expectation.new(nil, :my_method) + expectation2 = Expectation.new(nil, :my_method) + define_instance_method(expectation1, :invocations_never_allowed?) { true } + define_instance_method(expectation2, :invocations_never_allowed?) { false } + expectation_list.add(expectation1) + expectation_list.add(expectation2) + assert_same expectation1, expectation_list.match_never_allowing_invocation(Invocation.new(:irrelevant, :my_method)) + end + def test_should_combine_two_expectation_lists_into_one expectation_list1 = ExpectationList.new expectation_list2 = ExpectationList.new diff --git a/test/unit/expectation_test.rb b/test/unit/expectation_test.rb index 55716d89..04dfe78f 100644 --- a/test/unit/expectation_test.rb +++ b/test/unit/expectation_test.rb @@ -88,6 +88,13 @@ def test_should_allow_invocations_until_expected_invocation_count_is_a_range_fro assert !expectation.invocations_allowed? end + def test_should_never_allow_invocations + expectation = new_expectation.never + assert expectation.invocations_never_allowed? + invoke(expectation) + assert expectation.invocations_never_allowed? + end + def test_should_store_provided_backtrace backtrace = Object.new assert_equal backtrace, Expectation.new(nil, :expected_method, backtrace).backtrace