Skip to content

Commit

Permalink
Encapsulate invocations inside cardinality
Browse files Browse the repository at this point in the history
Also handles Expectaction#throws scenario.

PR: #410

Second bunch of commits from #394.

Co-authored-by: Nitish Rathi <[email protected]>
  • Loading branch information
floehopper and nitishr committed Nov 16, 2019
2 parents 00f0540 + 9e449b0 commit 4470825
Show file tree
Hide file tree
Showing 20 changed files with 171 additions and 106 deletions.
35 changes: 24 additions & 11 deletions lib/mocha/cardinality.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,42 @@ def times(range_or_count)
def initialize(required, maximum)
@required = required
@maximum = maximum
@invocations = []
end

def invocations_allowed?(invocation_count)
invocation_count < maximum
def <<(invocation)
@invocations << invocation
end

def satisfied?(invocations_so_far)
invocations_so_far >= required
def invocations_allowed?
@invocations.size < maximum
end

def satisfied?
@invocations.size >= required
end

def needs_verifying?
!allowed_any_number_of_times?
end

def verified?(invocation_count)
(invocation_count >= required) && (invocation_count <= maximum)
def verified?
(@invocations.size >= required) && (@invocations.size <= maximum)
end

def allowed_any_number_of_times?
required.zero? && infinite?(maximum)
end

def used?(invocation_count)
(invocation_count > 0) || maximum.zero?
def used?
@invocations.any? || maximum.zero?
end

def mocha_inspect
def anticipated_times
if allowed_any_number_of_times?
'allowed any number of times'
elsif required.zero? && maximum.zero?
'expected never'
"expected #{times(maximum)}"
elsif required == maximum
"expected exactly #{times(required)}"
elsif infinite?(maximum)
Expand All @@ -68,13 +73,21 @@ def mocha_inspect
end
end

def invoked_times
"invoked #{times(@invocations.size)}"
end

def actual_invocations
@invocations.map(&:mocha_inspect).join
end

protected

attr_reader :required, :maximum

def times(number)
case number
when 0 then 'no times'
when 0 then 'never'
when 1 then 'once'
when 2 then 'twice'
else "#{number} times"
Expand Down
3 changes: 2 additions & 1 deletion lib/mocha/exception_raiser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ def initialize(exception, message)
@message = message
end

def evaluate
def evaluate(invocation)
invocation.raised(@exception)
raise @exception, @exception.to_s if @exception.is_a?(Module) && (@exception < Interrupt)
raise @exception, @message if @message
raise @exception
Expand Down
27 changes: 7 additions & 20 deletions lib/mocha/expectation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,6 @@ def initialize(mock, expected_method_name, backtrace = nil)
@return_values = ReturnValues.new
@yield_parameters = YieldParameters.new
@backtrace = backtrace || caller
@invocations = []
end

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

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

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

# @private
def invoke(*arguments)
perform_side_effects
invocation = Invocation.new(method_name, @yield_parameters, @return_values)
@invocations << invocation
@cardinality << 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?(@invocations.size)
@cardinality.verified?
end

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

# @private
Expand All @@ -593,17 +592,9 @@ def inspect

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

Expand All @@ -617,9 +608,5 @@ def method_signature
def method_name
"#{@mock.mocha_inspect}.#{@method_matcher.mocha_inspect}"
end

def invocations
@invocations.map(&:mocha_inspect).join
end
end
end
23 changes: 18 additions & 5 deletions lib/mocha/invocation.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
require 'mocha/parameters_matcher'
require 'mocha/raised_exception'
require 'mocha/return_values'
require 'mocha/thrown_object'
require 'mocha/yield_parameters'

module Mocha
class Invocation
def initialize(method_name, yield_parameters, return_values)
def initialize(method_name, yield_parameters = YieldParameters.new, return_values = ReturnValues.new)
@method_name = method_name
@yield_parameters = yield_parameters
@return_values = return_values
@yields = []
@result = nil
end

def call(*arguments)
Expand All @@ -16,10 +20,19 @@ def call(*arguments)
@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
@return_values.next(self)
end

def returned(value)
@result = value
end

def raised(exception)
@result = RaisedException.new(exception)
end

def threw(tag, value)
@result = ThrownObject.new(tag, value)
end

def mocha_inspect
Expand Down
6 changes: 3 additions & 3 deletions lib/mocha/return_values.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ def initialize(*values)
@values = values
end

def next
def next(invocation)
case @values.length
when 0 then nil
when 1 then @values.first.evaluate
else @values.shift.evaluate
when 1 then @values.first.evaluate(invocation)
else @values.shift.evaluate(invocation)
end
end

Expand Down
3 changes: 2 additions & 1 deletion lib/mocha/single_return_value.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ def initialize(value)
@value = value
end

def evaluate
def evaluate(invocation)
invocation.returned(@value)
@value
end
end
Expand Down
3 changes: 2 additions & 1 deletion lib/mocha/thrower.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ def initialize(tag, object = nil)
@object = object
end

def evaluate
def evaluate(invocation)
invocation.threw(@tag, @object)
throw @tag, @object
end
end
Expand Down
12 changes: 12 additions & 0 deletions lib/mocha/thrown_object.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Mocha
class ThrownObject
def initialize(tag, value = nil)
@tag = tag
@value = value
end

def mocha_inspect
"threw (#{@tag.mocha_inspect}, #{@value.mocha_inspect})"
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,37 @@ 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.stubs(:bar).with(any_parameters).returns('f').raises(StandardError).throws(:tag, 'value')

foo.bar(1, 2)
assert_raise(StandardError) { foo.bar(3, 4) }
assert_throws(:tag) { foo.bar(5, 6) }
end
assert_invocations(
test_result,
'- allowed any number of times, invoked twice: #<Mock:foo>.bar(any_parameters)',
'- allowed any number of times, invoked 3 times: #<Mock:foo>.bar(any_parameters)',
' - #<Mock:foo>.bar(1, 2) # => "f"',
' - #<Mock:foo>.bar(3, 4) # => raised StandardError'
' - #<Mock:foo>.bar(3, 4) # => raised StandardError',
' - #<Mock:foo>.bar(5, 6) # => threw (:tag, "value")'
)
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.stubs(:bar).with(any_parameters).multiple_yields(%w[b c], %w[d e]).returns('f').raises(StandardError).throws(:tag, 'value')

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

Expand Down
2 changes: 1 addition & 1 deletion test/acceptance/exception_rescue_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_unsatisfied_expectation_exception_is_not_caught_by_standard_rescue
assert_equal [
'not all expectations were satisfied',
'unsatisfied expectations:',
'- expected exactly once, not yet invoked: #<Mock:mock>.some_method(any_parameters)'
'- expected exactly once, invoked never: #<Mock:mock>.some_method(any_parameters)'
], test_result.failure_message_lines
end
end
4 changes: 2 additions & 2 deletions test/acceptance/expected_invocation_count_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def test_should_fail_if_method_is_expected_at_least_once_but_is_never_called
assert_equal [
'not all expectations were satisfied',
'unsatisfied expectations:',
'- expected at least once, not yet invoked: #<Mock:mock>.method(any_parameters)'
'- expected at least once, invoked never: #<Mock:mock>.method(any_parameters)'
], test_result.failure_message_lines
end

Expand Down Expand Up @@ -224,7 +224,7 @@ def test_should_fail_fast_if_there_is_no_matching_expectation
assert_equal [
'unexpected invocation: #<Mock:mock>.method()',
'unsatisfied expectations:',
'- expected exactly once, not yet invoked: #<Mock:mock>.method(1)'
'- expected exactly once, invoked never: #<Mock:mock>.method(1)'
], test_result.failure_message_lines
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def my_instance_method; end
assert_equal [
'not all expectations were satisfied',
'unsatisfied expectations:',
'- expected exactly once, not yet invoked: #<AnyInstance:superklass>.my_instance_method(any_parameters)'
'- expected exactly once, invoked never: #<AnyInstance:superklass>.my_instance_method(any_parameters)'
], test_result.failure_message_lines
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def self.my_class_method; end
assert_equal [
'not all expectations were satisfied',
'unsatisfied expectations:',
'- expected exactly once, not yet invoked: superklass.my_class_method(any_parameters)'
'- expected exactly once, invoked never: superklass.my_class_method(any_parameters)'
], test_result.failure_message_lines
end
# rubocop:enable Lint/DuplicateMethods
Expand Down
6 changes: 3 additions & 3 deletions test/integration/shared_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def test_mock_object_unsatisfied_expectation
assert_equal [
'not all expectations were satisfied',
'unsatisfied expectations:',
'- expected exactly once, not yet invoked: #<Mock:expecting invocation>.expected(any_parameters)'
'- expected exactly once, invoked never: #<Mock:expecting invocation>.expected(any_parameters)'
], test_result.failure_message_lines
end

Expand Down Expand Up @@ -101,7 +101,7 @@ def test_mock_object_unsatisfied_expectation_in_setup
assert_equal [
'not all expectations were satisfied',
'unsatisfied expectations:',
'- expected exactly once, not yet invoked: #<Mock:expecting invocation>.expected(any_parameters)'
'- expected exactly once, invoked never: #<Mock:expecting invocation>.expected(any_parameters)'
], test_result.failure_message_lines
end

Expand Down Expand Up @@ -151,7 +151,7 @@ def test_real_object_unsatisfied_expectation
assert_equal [
'not all expectations were satisfied',
'unsatisfied expectations:',
"- expected exactly once, not yet invoked: #{object.mocha_inspect}.expected(any_parameters)"
"- expected exactly once, invoked never: #{object.mocha_inspect}.expected(any_parameters)"
], test_result.failure_message_lines
end

Expand Down
Loading

0 comments on commit 4470825

Please sign in to comment.