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

Reduce surface & size of Cardinality #481

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
43 changes: 12 additions & 31 deletions lib/mocha/cardinality.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,14 @@ class Cardinality
INFINITY = 1 / 0.0

def initialize(required = 0, maximum = INFINITY)
update(required, maximum)
range(required, maximum)
@invocations = []
end

def exactly(count)
update(count, count)
end

def at_least(count)
update(count, INFINITY)
end

def at_most(count)
update(0, count)
end

def times(range_or_count)
case range_or_count
when Range then update(range_or_count.first, range_or_count.last)
else update(range_or_count, range_or_count)
end
def range(at_least = 0, at_most = INFINITY)
@required = at_least
@maximum = at_most
self
end

def <<(invocation)
Expand Down Expand Up @@ -59,21 +46,21 @@ def anticipated_times
if allowed_any_number_of_times?
'allowed any number of times'
elsif required.zero? && maximum.zero?
"expected #{count(maximum)}"
"expected #{times(maximum)}"
elsif required == maximum
"expected exactly #{count(required)}"
"expected exactly #{times(required)}"
elsif infinite?(maximum)
"expected at least #{count(required)}"
"expected at least #{times(required)}"
elsif required.zero?
"expected at most #{count(maximum)}"
"expected at most #{times(maximum)}"
else
"expected between #{required} and #{count(maximum)}"
"expected between #{required} and #{times(maximum)}"
end
end
# rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity

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

def actual_invocations
Expand All @@ -84,7 +71,7 @@ def actual_invocations

attr_reader :required, :maximum

def count(number)
def times(number)
case number
when 0 then 'never'
when 1 then 'once'
Expand All @@ -93,12 +80,6 @@ def count(number)
end
end

def update(required, maximum)
@required = required
@maximum = maximum
self
end

def infinite?(number)
number.respond_to?(:infinite?) && number.infinite?
end
Expand Down
39 changes: 20 additions & 19 deletions lib/mocha/expectation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module Mocha
class Expectation
# Modifies expectation so that the number of calls to the expected method must be within a specific +range+.
#
# @param [Range,Integer] range specifies the allowable range in the number of expected invocations.
# @param [Range,Integer] range specifies the allowable number or range in the number of expected invocations.
# @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained.
#
# @example Specifying a specific number of expected invocations.
Expand All @@ -42,11 +42,12 @@ class Expectation
# object.expected_method
# # => verify fails
def times(range)
@cardinality.times(range)
range = range..range unless range.is_a?(Range)
@cardinality.range(range.first, range.last)
self
end

# Modifies expectation so that the expected method must be called exactly twice.
# Modifies expectation so that the expected method must be called exactly twice. Has the same effect as calling {times}(2).
#
# @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained.
#
Expand All @@ -68,11 +69,10 @@ def times(range)
# object.expected_method
# # => verify fails
def twice
@cardinality.exactly(2)
self
times(2)
end

# Modifies expectation so that the expected method must be called exactly once.
# Modifies expectation so that the expected method must be called exactly once. Has the same effect as calling {times}(1).
#
# Note that this is the default behaviour for an expectation, but you may wish to use it for clarity/emphasis.
#
Expand All @@ -93,11 +93,10 @@ def twice
# object.expects(:expected_method).once
# # => verify fails
def once
@cardinality.exactly(1)
self
times(1)
end

# Modifies expectation so that the expected method must never be called.
# Modifies expectation so that the expected method must never be called. Has the same effect as calling {times}(0).
#
# @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained.
#
Expand All @@ -110,11 +109,11 @@ def once
# object.expects(:expected_method).never
# # => verify succeeds
def never
@cardinality.exactly(0)
self
times(0)
end

# Modifies expectation so that the expected method must be called at least a +minimum_number_of_times+.
# Modifies expectation so that the expected method must be called at least a +minimum_number_of_times+ and at most any number of times.
# Has the same effect as calling {times}(+minimum_number_of_times+..Float::INFINITY).
#
# @param [Integer] minimum_number_of_times minimum number of expected invocations.
# @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained.
Expand All @@ -130,11 +129,12 @@ def never
# object.expected_method
# # => verify fails
def at_least(minimum_number_of_times)
@cardinality.at_least(minimum_number_of_times)
@cardinality.range(minimum_number_of_times)
self
end

# Modifies expectation so that the expected method must be called at least once.
# Modifies expectation so that the expected method must be called at least once and at most any number of times.
# Has the same effect as calling {at_least}(1).
#
# @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained.
#
Expand All @@ -151,7 +151,8 @@ def at_least_once
at_least(1)
end

# Modifies expectation so that the expected method must be called at most a +maximum_number_of_times+.
# Modifies expectation so that the expected method must be called from never to at most a +maximum_number_of_times+.
# Has the same effect as calling {times}(0..+maximum_number_of_times+).
#
# @param [Integer] maximum_number_of_times maximum number of expected invocations.
# @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained.
Expand All @@ -166,11 +167,11 @@ def at_least_once
# object.expects(:expected_method).at_most(2)
# 3.times { object.expected_method } # => unexpected invocation
def at_most(maximum_number_of_times)
@cardinality.at_most(maximum_number_of_times)
self
times(0..maximum_number_of_times)
end

# Modifies expectation so that the expected method must be called at most once.
# Modifies expectation so that the expected method must be called from never to at most once.
# Has the same effect as calling {at_most}(1).
#
# @return [Expectation] the same expectation, thereby allowing invocations of other {Expectation} methods to be chained.
#
Expand Down Expand Up @@ -547,7 +548,7 @@ def initialize(mock, expected_method_name, backtrace = nil)
@block_matcher = BlockMatchers::OptionalBlock.new
@ordering_constraints = []
@side_effects = []
@cardinality = Cardinality.new.exactly(1)
@cardinality = Cardinality.new.range(1, 1)
@return_values = ReturnValues.new
@yield_parameters = YieldParameters.new
@backtrace = backtrace || caller
Expand Down
38 changes: 19 additions & 19 deletions test/unit/cardinality_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,39 +34,39 @@ def test_should_be_satisfied_if_invocations_so_far_have_reached_required_thresho
end

def test_should_describe_cardinality_defined_using_at_least
assert_equal 'allowed any number of times', Cardinality.new.at_least(0).anticipated_times
assert_equal 'expected at least once', Cardinality.new.at_least(1).anticipated_times
assert_equal 'expected at least twice', Cardinality.new.at_least(2).anticipated_times
assert_equal 'expected at least 3 times', Cardinality.new.at_least(3).anticipated_times
assert_equal 'allowed any number of times', Cardinality.new.range(0).anticipated_times
assert_equal 'expected at least once', Cardinality.new.range(1).anticipated_times
assert_equal 'expected at least twice', Cardinality.new.range(2).anticipated_times
assert_equal 'expected at least 3 times', Cardinality.new.range(3).anticipated_times
end

def test_should_describe_cardinality_defined_using_at_most
assert_equal 'expected at most once', Cardinality.new.at_most(1).anticipated_times
assert_equal 'expected at most twice', Cardinality.new.at_most(2).anticipated_times
assert_equal 'expected at most 3 times', Cardinality.new.at_most(3).anticipated_times
assert_equal 'expected at most once', Cardinality.new.range(0, 1).anticipated_times
assert_equal 'expected at most twice', Cardinality.new.range(0, 2).anticipated_times
assert_equal 'expected at most 3 times', Cardinality.new.range(0, 3).anticipated_times
end

def test_should_describe_cardinality_defined_using_exactly
assert_equal 'expected never', Cardinality.new.exactly(0).anticipated_times
assert_equal 'expected exactly once', Cardinality.new.exactly(1).anticipated_times
assert_equal 'expected exactly twice', Cardinality.new.exactly(2).anticipated_times
assert_equal 'expected exactly 3 times', Cardinality.new.exactly(3).anticipated_times
assert_equal 'expected never', Cardinality.new.range(0, 0).anticipated_times
assert_equal 'expected exactly once', Cardinality.new.range(1, 1).anticipated_times
assert_equal 'expected exactly twice', Cardinality.new.range(2, 2).anticipated_times
assert_equal 'expected exactly 3 times', Cardinality.new.range(3, 3).anticipated_times
end

def test_should_describe_cardinality_defined_using_times_with_range
assert_equal 'expected between 2 and 4 times', Cardinality.new.times(2..4).anticipated_times
assert_equal 'expected between 1 and 3 times', Cardinality.new.times(1..3).anticipated_times
assert_equal 'expected between 2 and 4 times', Cardinality.new.range(2, 4).anticipated_times
assert_equal 'expected between 1 and 3 times', Cardinality.new.range(1, 3).anticipated_times
end

def test_should_need_verifying
assert Cardinality.new.exactly(2).needs_verifying?
assert Cardinality.new.at_least(3).needs_verifying?
assert Cardinality.new.at_most(2).needs_verifying?
assert Cardinality.new.times(4).needs_verifying?
assert Cardinality.new.times(2..4).needs_verifying?
assert Cardinality.new.range(2, 2).needs_verifying?
assert Cardinality.new.range(3).needs_verifying?
assert Cardinality.new.range(0, 2).needs_verifying?
assert Cardinality.new.range(4).needs_verifying?
assert Cardinality.new.range(2, 4).needs_verifying?
end

def test_should_not_need_verifying
assert_equal false, Cardinality.new.at_least(0).needs_verifying?
assert_equal false, Cardinality.new.range(0).needs_verifying?
end
end