Class: Matchi::Change::ByAtMost

Inherits:
Object
  • Object
show all
Defined in:
lib/matchi/change/by_at_most.rb

Overview

Note:

This matcher verifies maximum changes only. For exact changes, use By, and for minimum changes, use ByAtLeast.

Maximum delta matcher that verifies numeric changes don’t exceed a limit.

This matcher ensures that a numeric value changes by no more than the specified amount after executing a block of code. It’s particularly useful when testing operations where you want to enforce an upper bound on changes, such as rate limiting, resource consumption, or controlled increments.

Examples:

Testing controlled collection growth

items = []
matcher = Matchi::Change::ByAtMost.new(2) { items.size }
matcher.match? { items.push(1) }             # => true   # Changed by less than limit
matcher.match? { items.push(1, 2) }          # => true   # Changed by exactly limit
matcher.match? { items.push(1, 2, 3) }       # => false  # Changed by more than limit

Rate limiting

class RateLimiter
  def initialize
    @requests = 0
  end

  def process_batch(items)
    items.each do |item|
      break if @requests >= 3  # Rate limit
      process_item(item)
    end
  end

  private

  def process_item(item)
    @requests += 1
    # Processing logic...
  end

  def requests
    @requests
  end
end

limiter = RateLimiter.new
matcher = Matchi::Change::ByAtMost.new(3) { limiter.requests }
matcher.match? { limiter.process_batch([1, 2, 3, 4, 5]) } # => true

Resource consumption

class ResourcePool
  attr_reader :used

  def initialize
    @used = 0
  end

  def allocate(requested)
    available = 5 - @used  # Maximum pool size is 5
    granted = [requested, available].min
    @used += granted
    granted
  end
end

pool = ResourcePool.new
matcher = Matchi::Change::ByAtMost.new(2) { pool.used }
matcher.match? { pool.allocate(2) }  # => true
matcher.match? { pool.allocate(3) }  # => false

Score adjustments

class GameScore
  attr_reader :value

  def initialize
    @value = 100
  end

  def apply_penalty(amount)
    max_penalty = 10
    actual_penalty = [amount, max_penalty].min
    @value -= actual_penalty
  end
end

score = GameScore.new
matcher = Matchi::Change::ByAtMost.new(10) { -score.value }
matcher.match? { score.apply_penalty(5) }   # => true   # Small penalty
matcher.match? { score.apply_penalty(15) }  # => true   # Limited to max

See Also:

Instance Method Summary collapse

Constructor Details

#initialize(expected, &state) ⇒ ByAtMost

Initialize the matcher with a maximum allowed change and a state block.

Examples:

With integer maximum

ByAtMost.new(5) { counter.value }

With floating point maximum

ByAtMost.new(0.5) { temperature.celsius }

Parameters:

  • expected (Numeric)

    The maximum amount by which the value should change

  • state (Proc)

    Block that retrieves the current value

Raises:

  • (ArgumentError)

    if expected is not a Numeric

  • (ArgumentError)

    if expected is negative

  • (ArgumentError)

    if no state block is provided



114
115
116
117
118
119
120
121
# File 'lib/matchi/change/by_at_most.rb', line 114

def initialize(expected, &state)
  raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
  raise ::ArgumentError, "a block must be provided" unless block_given?
  raise ::ArgumentError, "expected must be non-negative" if expected.negative?

  @expected = expected
  @state = state
end

Instance Method Details

#match? { ... } ⇒ Boolean

Checks if the value changes by no more than the expected amount.

This method compares the value before and after executing the provided block, ensuring that the absolute difference is less than or equal to the expected maximum. This is useful for enforcing upper bounds on state changes.

Examples:

Basic usage with growth

users = []
matcher = ByAtMost.new(2) { users.size }
matcher.match? { users.push('alice') }  # => true

With negative changes

stock = 10
matcher = ByAtMost.new(3) { stock }
matcher.match? { stock -= 2 }  # => true

Yields:

  • Block that should cause the state change

Yield Returns:

  • (Object)

    The result of the block (not used)

Returns:

  • (Boolean)

    true if the value changed by at most the expected amount

Raises:

  • (ArgumentError)

    if no block is provided



147
148
149
150
151
152
153
154
155
# File 'lib/matchi/change/by_at_most.rb', line 147

def match?
  raise ::ArgumentError, "a block must be provided" unless block_given?

  value_before = @state.call
  yield
  value_after = @state.call

  @expected >= (value_after - value_before)
end

#to_sString

Returns a human-readable description of the matcher.

Examples:

ByAtMost.new(5).to_s    # => "change by at most 5"
ByAtMost.new(2.5).to_s  # => "change by at most 2.5"

Returns:

  • (String)

    A string describing what this matcher verifies



166
167
168
# File 'lib/matchi/change/by_at_most.rb', line 166

def to_s
  "change by at most #{@expected.inspect}"
end