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

Display matching invocations alongside expectations #394

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7149c23
display return values of invocations
nitishr Oct 23, 2019
84a844c
display raised exceptions of invocations
nitishr Oct 23, 2019
2bd2538
Prepare to display yields of invocations
nitishr Oct 24, 2019
24f9c11
Move yielding to invocation
nitishr Oct 24, 2019
1ff477c
display yields of matching invocations
nitishr Oct 24, 2019
1976f01
display arguments of matching invocations
nitishr Oct 24, 2019
de777f3
Reduce granularity and redundancy of tests
nitishr Oct 24, 2019
4b133ec
Replace invocation_count with invocations.size
nitishr Oct 24, 2019
90c57c3
add test for yielding & returning nothing
nitishr Oct 24, 2019
26e4397
DRY up display matching invocations alongside
nitishr Oct 24, 2019
78cfb20
Prep to move invocation size based logic to Cardinality
nitishr Oct 30, 2019
41142f7
Move invocation size based logic to Cardinality
nitishr Oct 30, 2019
b8e07cf
Move invocations list to Cardinality
nitishr Oct 30, 2019
e7a9618
Infer invocation count from invocations size
nitishr Oct 30, 2019
2f7011a
Encapsulate invocations inside Cardinality
nitishr Oct 30, 2019
aa5ed0e
rename methods to better describe intention
nitishr Oct 30, 2019
1aa23bd
rename methods to better describe intention
nitishr Oct 30, 2019
f1922d4
rename methods to better describe intention
nitishr Oct 30, 2019
77aa25e
interpolate rather than append simple expressions
nitishr Oct 30, 2019
fd290ea
use same times logic for non-zero configured & invoked
nitishr Oct 30, 2019
d5c7e5b
times is never invoked with 0
nitishr Oct 30, 2019
7051809
more expressive & concise conditionals & booleans
nitishr Oct 30, 2019
21ac1f9
use consistent phrasing for configured & invoked times
nitishr Oct 30, 2019
0d57961
reduce test's knowledge of irrelevant details
nitishr Nov 4, 2019
9007a97
record result of invocation before 'returning' it
nitishr Nov 5, 2019
4e32c0c
display thrown object
nitishr Nov 5, 2019
6284acb
don't assert existence of other lines of failure messages
nitishr Nov 6, 2019
c061de0
no need for blocks when expectation doesn't yield
nitishr Nov 6, 2019
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
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
39 changes: 18 additions & 21 deletions lib/mocha/expectation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -512,7 +513,6 @@ 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
Expand Down Expand Up @@ -555,33 +555,31 @@ def match?(actual_method_name, *actual_parameters)

# @private
def invocations_allowed?
@cardinality.invocations_allowed?(@invocation_count)
@cardinality.invocations_allowed?
end

# @private
def satisfied?
@cardinality.satisfied?(@invocation_count)
@cardinality.satisfied?
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)
@cardinality << invocation
invocation.call(*arguments) { |*yield_args| yield(*yield_args) }
floehopper marked this conversation as resolved.
Show resolved Hide resolved
end

# @private
def verified?(assertion_counter = nil)
assertion_counter.increment if assertion_counter && @cardinality.needs_verifying?
@cardinality.verified?(@invocation_count)
@cardinality.verified?
end

# @private
def used?
@cardinality.used?(@invocation_count)
@cardinality.used?
end

# @private
Expand All @@ -593,22 +591,21 @@ def inspect

# @private
floehopper marked this conversation as resolved.
Show resolved Hide resolved
def mocha_inspect
message = "#{@cardinality.mocha_inspect}, "
message << case @invocation_count
when 0 then 'not yet invoked'
when 1 then 'invoked once'
when 2 then 'invoked twice'
else "invoked #{@invocation_count} 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 << @cardinality.actual_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
end
end
50 changes: 50 additions & 0 deletions lib/mocha/invocation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
require 'mocha/parameters_matcher'
require 'mocha/raised_exception'
require 'mocha/return_values'
require 'mocha/thrown_object'
require 'mocha/yield_parameters'

module Mocha
class Invocation
# @private
floehopper marked this conversation as resolved.
Show resolved Hide resolved
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

# @private
def call(*arguments)
@arguments = ParametersMatcher.new(arguments)
@yield_parameters.next_invocation.each do |yield_parameters|
@yields << ParametersMatcher.new(yield_parameters)
yield(*yield_parameters)
end
@return_values.next(self)
end

# @private
def returned(value)
@result = value
end

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

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

# @private
def mocha_inspect
desc = "\n - #{@method_name}#{@arguments.mocha_inspect} # => #{@result.mocha_inspect}"
floehopper marked this conversation as resolved.
Show resolved Hide resolved
desc << " after yielding #{@yields.map(&:mocha_inspect).join(', then ')}" if @yields.any?
desc
end
end
end
4 changes: 2 additions & 2 deletions lib/mocha/mock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -305,11 +305,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)
floehopper marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
11 changes: 11 additions & 0 deletions lib/mocha/raised_exception.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Mocha
class RaisedException
def initialize(exception)
@exception = exception
end

def mocha_inspect
"raised #{@exception}"
end
end
end
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
@@ -0,0 +1,76 @@
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
floehopper marked this conversation as resolved.
Show resolved Hide resolved
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).throws(:tag, 'value')

foo.bar(1, 2)
assert_raise(StandardError) { foo.bar(3, 4) }
assert_throws(:tag) { foo.bar(5, 6) }
end
assert_invocations(
floehopper marked this conversation as resolved.
Show resolved Hide resolved
test_result,
'- 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(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).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 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(5, 6) # => threw (:tag, "value") after yielding ("b", "c"), then ("d", "e")'
)
end

def test_should_display_empty_yield_and_return
floehopper marked this conversation as resolved.
Show resolved Hide resolved
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: #<Mock:foo>.bar(any_parameters)',
' - #<Mock:foo>.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
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
Loading