matchi 4.2.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,133 +1,111 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "constant"
4
+
3
5
  module Matchi
4
- # Exception matcher that verifies if a block of code raises a specific exception.
5
- #
6
- # This matcher ensures that code raises an expected exception by executing it within
7
- # a controlled environment. It supports matching against specific exception classes
8
- # and their subclasses, providing a reliable way to test error handling.
9
- #
10
- # @example Basic usage with standard exceptions
11
- # matcher = Matchi::RaiseException.new(ArgumentError)
12
- # matcher.match? { raise ArgumentError } # => true
13
- # matcher.match? { raise RuntimeError } # => false
14
- # matcher.match? { "no error" } # => false
15
- #
16
- # @example Working with inheritance hierarchy
17
- # class CustomError < StandardError; end
18
- # class SpecificError < CustomError; end
19
- #
20
- # matcher = Matchi::RaiseException.new(CustomError)
21
- # matcher.match? { raise CustomError } # => true
22
- # matcher.match? { raise SpecificError } # => true
23
- # matcher.match? { raise StandardError } # => false
24
- #
25
- # @example Using string class names
26
- # matcher = Matchi::RaiseException.new("ArgumentError")
27
- # matcher.match? { raise ArgumentError } # => true
6
+ # Exception matcher: passes when evaluating the block raises an exception
7
+ # of the expected class or of one of its descendants (`Module#===`, so the
8
+ # exception cannot lie about its class), optionally with an expected
9
+ # message: a String the message must be `eql?` to, or a Regexp it must
10
+ # match.
28
11
  #
29
- # @example With custom exception hierarchies
30
- # module Api
31
- # class Error < StandardError; end
32
- # class AuthenticationError < Error; end
33
- # end
12
+ # The matcher rescues an exception of the expected class, and any
13
+ # `StandardError`, `ScriptError`, `SystemStackError` or `SystemExit`,
14
+ # which it then compares with the expected class. Anything else, an
15
+ # `Interrupt`, a signal, a `NoMemoryError`, the exception with which an
16
+ # enclosing `Timeout` ends its block, goes through as it would with any
17
+ # other matcher, unless that is the class expected. The expected class can
18
+ # be given directly, or by name; see {Matchi::Constant}.
34
19
  #
35
- # matcher = Matchi::RaiseException.new("Api::Error")
36
- # matcher.match? { raise Api::AuthenticationError } # => true
37
- #
38
- # @see https://ruby-doc.org/core/Exception.html
39
- # @see https://ruby-doc.org/core/StandardError.html
20
+ # @example
21
+ # Matchi::RaiseException.new(ZeroDivisionError).match? { 1 / 0 } # => true
22
+ # Matchi::RaiseException.new(:StandardError).match? { 1 / 0 } # => true
23
+ # Matchi::RaiseException.new(ZeroDivisionError, /by 0/).match? { 1 / 0 } # => true
24
+ # Matchi::RaiseException.new(ZeroDivisionError, "nope").match? { 1 / 0 } # => false
25
+ # Matchi::RaiseException.new(IOError).match? { 1 / 0 } # => false
40
26
  class RaiseException
41
- # Initialize the matcher with an exception class.
42
- #
43
- # @api public
44
- #
45
- # @param expected [Exception, #to_s] The expected exception class or its name
46
- # Can be provided as a Class, String, or Symbol
47
- #
48
- # @raise [ArgumentError] if the class name doesn't start with an uppercase letter
49
- #
50
- # @return [RaiseException] a new instance of the matcher
51
- #
52
- # @example
53
- # RaiseException.new(ArgumentError) # Using class
54
- # RaiseException.new("ArgumentError") # Using string
55
- # RaiseException.new(:ArgumentError) # Using symbol
56
- # RaiseException.new("Api::CustomError") # Using namespaced class
57
- def initialize(expected)
58
- @expected = String(expected)
59
- return if /\A[A-Z]/.match?(@expected)
60
-
61
- raise ::ArgumentError,
62
- "expected must start with an uppercase letter (got: #{@expected})"
27
+ # @param expected [Class, String, Symbol] the expected exception class, or its name
28
+ # @param message [String, Regexp, nil] the expected message, exact or as a pattern
29
+ # @raise [ArgumentError] when expected is neither an Exception class nor a constant name,
30
+ # or message is neither a String nor a Regexp
31
+ def initialize(expected, message = nil)
32
+ @expected = Constant.normalize(expected, ::Class)
33
+ @message = message
34
+ validate_class!
35
+ validate_message!
63
36
  end
64
37
 
65
- # Checks if the yielded block raises the expected exception.
66
- #
67
- # This method executes the provided block within a begin/rescue clause and verifies
68
- # that it raises an exception of the expected type. It also handles inheritance,
69
- # allowing subclasses of the expected exception to match.
70
- #
71
- # @api public
72
- #
73
- # @yield [] Block that should raise an exception
74
- # @yieldreturn [Object] The result of the block (if it doesn't raise)
75
- #
76
- # @return [Boolean] true if the block raises the expected exception
77
- #
78
- # @raise [ArgumentError] if no block is provided
79
- # @raise [ArgumentError] if expected exception class doesn't inherit from Exception
80
- # @raise [NameError] if the expected exception class cannot be found
81
- #
82
- # @example Standard usage
83
- # matcher = RaiseException.new(ArgumentError)
84
- # matcher.match? { raise ArgumentError } # => true
85
- #
86
- # @example With inheritance
87
- # matcher = RaiseException.new(StandardError)
88
- # matcher.match? { raise ArgumentError } # => true
89
- #
90
- # @example Without exception
91
- # matcher = RaiseException.new(StandardError)
92
- # matcher.match? { "no error" } # => false
38
+ # @yieldreturn [Object] ignored, the block is evaluated for its exception
39
+ # @return [Boolean]
40
+ # @raise [ArgumentError] when no block is given
41
+ # @raise [NameError] when the name is not defined
42
+ # @raise [TypeError] when the name does not resolve to an Exception class
93
43
  def match?
94
44
  raise ::ArgumentError, "a block must be provided" unless block_given?
95
45
 
46
+ # Resolved before the block, so that a NameError from the name is not
47
+ # mistaken for an exception raised by the block.
96
48
  klass = expected_class
97
- raise ::ArgumentError, "expected exception class must inherit from Exception" unless klass <= ::Exception
98
49
 
50
+ # The expected class first, whatever it is; then what a block raises
51
+ # on its own, the exceptions Defi keeps too.
99
52
  begin
100
53
  yield
101
54
  false
102
- rescue ::Exception => e # rubocop:disable Lint/RescueException
103
- e.class <= klass
55
+ rescue klass => e
56
+ message?(e)
57
+ rescue ::StandardError, ::ScriptError, ::SystemStackError, ::SystemExit
58
+ false
104
59
  end
105
60
  end
106
61
 
107
- # Returns a human-readable description of the matcher.
108
- #
109
- # @api public
110
- #
111
- # @return [String] A string describing what this matcher verifies
112
- #
113
- # @example
114
- # RaiseException.new(ArgumentError).to_s # => "raise exception ArgumentError"
62
+ # @return [String]
115
63
  def to_s
116
- "raise exception #{@expected}"
64
+ return "raise exception #{Constant.label(@expected)}" if @message.nil?
65
+
66
+ "raise exception #{Constant.label(@expected)} with message #{@message.inspect}"
117
67
  end
118
68
 
119
69
  private
120
70
 
121
- # Resolves the expected class name to an actual Exception class.
122
- #
123
- # @api private
124
- #
125
- # @return [Class] The resolved exception class
126
- # @raise [NameError] If the class name cannot be resolved to an actual class
127
- #
128
- # @note This method handles both string and symbol class names through constant resolution
71
+ def validate_class!
72
+ return unless ::Class === @expected
73
+ return if @expected <= ::Exception
74
+
75
+ raise ::ArgumentError, "expected must inherit from Exception (got: #{@expected.inspect})"
76
+ end
77
+
78
+ def validate_message!
79
+ return if @message.nil? || ::String === @message || ::Regexp === @message
80
+
81
+ raise ::ArgumentError, "message must be a String or a Regexp (got: #{@message.inspect})"
82
+ end
83
+
129
84
  def expected_class
130
- ::Object.const_get(@expected)
85
+ return @expected if ::Class === @expected
86
+
87
+ klass = Constant.resolve(@expected, ::Class)
88
+ raise ::TypeError, "#{@expected} is not an Exception class" unless klass <= ::Exception
89
+
90
+ klass
91
+ end
92
+
93
+ # The message is whatever the exception answers to `message`, which is
94
+ # its own data, as with any exception; it must be a String, copied so
95
+ # that a subclass has no say, and valid. The expected String or Regexp
96
+ # receives the comparison.
97
+ def message?(exception)
98
+ return true if @message.nil?
99
+
100
+ message = exception.message
101
+ return false unless ::String === message
102
+
103
+ message = ::String.new(message)
104
+ return false unless message.valid_encoding?
105
+
106
+ ::String === @message ? @message.eql?(message) : @message.match?(message)
107
+ rescue ::Encoding::CompatibilityError
108
+ false
131
109
  end
132
110
  end
133
111
  end
@@ -1,114 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "constant"
4
+
3
5
  module Matchi
4
- # Custom predicate matcher that validates values against an arbitrary condition.
5
- #
6
- # This matcher provides a flexible way to test values against custom conditions
7
- # defined in a block. Unlike specific matchers that test for predetermined
8
- # conditions, Satisfy allows you to define any custom validation logic at runtime.
9
- # This makes it particularly useful for complex or composite conditions that
10
- # aren't covered by other matchers.
11
- #
12
- # @example Basic numeric validation
13
- # matcher = Matchi::Satisfy.new { |n| n.positive? && n.even? }
14
- # matcher.match? { 2 } # => true
15
- # matcher.match? { -2 } # => false
16
- # matcher.match? { 3 } # => false
17
- #
18
- # @example String pattern validation
19
- # matcher = Matchi::Satisfy.new { |s| s.start_with?("test") && s.length < 10 }
20
- # matcher.match? { "test_123" } # => true
21
- # matcher.match? { "test_12345" } # => false
22
- # matcher.match? { "other" } # => false
6
+ # Custom matcher: passes when the given block, called with the actual value,
7
+ # returns `true`. The block must return `true` or `false`; any other value
8
+ # is an error, not a truthy pass.
23
9
  #
24
- # @example Complex object validation
25
- # class User
26
- # attr_reader :age, :name
27
- # def initialize(name, age)
28
- # @name, @age = name, age
29
- # end
30
- # end
31
- #
32
- # matcher = Matchi::Satisfy.new { |user|
33
- # user.age >= 18 && user.name.length >= 2
34
- # }
35
- # matcher.match? { User.new("Alice", 25) } # => true
36
- # matcher.match? { User.new("B", 20) } # => false
37
- #
38
- # @example Using with collections
39
- # matcher = Matchi::Satisfy.new { |arr|
40
- # arr.all? { |x| x.is_a?(Integer) } && arr.sum.even?
41
- # }
42
- # matcher.match? { [2, 4, 6] } # => true
43
- # matcher.match? { [1, 3, 5] } # => false
44
- # matcher.match? { [1, "2", 3] } # => false
45
- #
46
- # @see https://ruby-doc.org/core/Proc.html
47
- # @see Matchi::Predicate For testing existing predicate methods
10
+ # @example
11
+ # Matchi::Satisfy.new { |value| value > 41 }.match? { 42 } # => true
48
12
  class Satisfy
49
- # Initialize the matcher with a validation block.
50
- #
51
- # @api public
52
- #
53
- # @yield [Object] Block that defines the validation condition
54
- # @yieldparam value The value to validate
55
- # @yieldreturn [Boolean] true if the value meets the condition
56
- #
57
- # @raise [ArgumentError] if no block is provided
58
- #
59
- # @return [Satisfy] a new instance of the matcher
60
- #
61
- # @example Simple numeric validation
62
- # Satisfy.new { |n| n > 0 }
63
- #
64
- # @example Complex condition
65
- # Satisfy.new { |obj|
66
- # obj.respond_to?(:length) && obj.length.between?(2, 10)
67
- # }
13
+ # @yieldparam actual [Object] the actual value
14
+ # @yieldreturn [Boolean]
15
+ # @raise [ArgumentError] when no block is given
68
16
  def initialize(&block)
69
17
  raise ::ArgumentError, "a block must be provided" unless block_given?
70
18
 
71
- @expected = block
19
+ @predicate = block
72
20
  end
73
21
 
74
- # Checks if the yielded value satisfies the validation block.
75
- #
76
- # This method passes the value returned by the provided block to the
77
- # validation block defined at initialization. The matcher succeeds if
78
- # the validation block returns a truthy value.
79
- #
80
- # @api public
81
- #
82
- # @yield [] Block that returns the value to validate
83
- # @yieldreturn [Object] The value to check against the condition
84
- #
85
- # @return [Boolean] true if the value satisfies the condition
86
- #
87
- # @raise [ArgumentError] if no block is provided
88
- #
89
- # @example Using with direct values
90
- # matcher = Satisfy.new { |n| n.positive? }
91
- # matcher.match? { 42 } # => true
92
- # matcher.match? { -1 } # => false
93
- #
94
- # @example Using with computed values
95
- # matcher = Satisfy.new { |s| s.length.even? }
96
- # matcher.match? { "test".upcase } # => true
97
- # matcher.match? { "a" * 3 } # => false
22
+ # @yieldreturn [Object] the actual value
23
+ # @return [Boolean]
24
+ # @raise [ArgumentError] when no block is given
25
+ # @raise [TypeError] when the predicate returns something other than true or false
98
26
  def match?
99
27
  raise ::ArgumentError, "a block must be provided" unless block_given?
100
28
 
101
- @expected.call(yield)
29
+ result = @predicate.call(yield)
30
+ return result if true.equal?(result) || false.equal?(result)
31
+
32
+ klass = ::Kernel.instance_method(:class).bind_call(result)
33
+ raise ::TypeError, "the block must return true or false (got a #{Constant.label(klass)})"
102
34
  end
103
35
 
104
- # Returns a human-readable description of the matcher.
105
- #
106
- # @api public
107
- #
108
- # @return [String] A string describing what this matcher verifies
109
- #
110
- # @example
111
- # Satisfy.new { |n| n > 0 }.to_s # => "satisfy &block"
36
+ # @return [String]
112
37
  def to_s
113
38
  "satisfy &block"
114
39
  end
data/lib/matchi.rb CHANGED
@@ -1,65 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Main namespace for the Matchi gem, providing a collection of expectation matchers.
4
- #
5
- # Matchi is a framework-agnostic Ruby library that offers a comprehensive set of
6
- # matchers for testing and verification purposes. Each matcher follows a consistent
7
- # interface pattern, making them easy to use and extend.
8
- #
9
- # @example Basic usage with equality matcher
10
- # Matchi::Eq.new("hello").match? { "hello" } # => true
11
- # Matchi::Eq.new("hello").match? { "world" } # => false
12
- #
13
- # @example Type checking with inheritance awareness
14
- # Matchi::BeAKindOf.new(Numeric).match? { 42 } # => true
15
- # Matchi::BeAKindOf.new(String).match? { 42 } # => false
16
- #
17
- # @example Verifying state changes
18
- # array = []
19
- # Matchi::Change.new(array, :length).by(2).match? { array.push(1, 2) } # => true
20
- #
21
- # @example Pattern matching with regular expressions
22
- # Matchi::Match.new(/^test/).match? { "test_string" } # => true
23
- #
24
- # Each matcher in the Matchi ecosystem implements a consistent interface:
25
- # - An initializer that sets up the expected values or conditions
26
- # - A #match? method that takes a block and returns a boolean
27
- # - An optional #to_s method that provides a human-readable description
28
- #
29
- # @example Creating a custom matcher
30
- # module Matchi
31
- # class BePositive
32
- # def match?
33
- # raise ArgumentError, "a block must be provided" unless block_given?
34
- # yield.positive?
35
- # end
36
- #
37
- # def to_s
38
- # "be positive"
39
- # end
40
- # end
41
- # end
42
- #
43
- # @see Matchi::Be
44
- # @see Matchi::BeAKindOf
45
- # @see Matchi::BeAnInstanceOf
46
- # @see Matchi::BeWithin
47
- # @see Matchi::Change
48
- # @see Matchi::Eq
49
- # @see Matchi::Match
50
- # @see Matchi::Predicate
51
- # @see Matchi::RaiseException
52
- # @see Matchi::Satisfy
3
+ # Framework-agnostic matchers for Ruby.
4
+ #
5
+ # A matcher is any object that responds to `match?` with a block and to
6
+ # `to_s`. The block is evaluated once and yields the actual value, or, for
7
+ # `Change` and `RaiseException`, produces the effect to observe; the matcher
8
+ # returns `true` or `false`. The expected value controls the comparison
9
+ # wherever a comparison takes place, so a hostile actual value cannot forge
10
+ # a match; `Predicate` and the `Change` refinements by delta, which use
11
+ # values produced by the object under test, depend on its honesty.
12
+ #
13
+ # @example
14
+ # Matchi::Eq.new(42).match? { 42 } # => true
53
15
  module Matchi
54
16
  end
55
17
 
56
- require "matchi/be"
57
- require "matchi/be_a_kind_of"
58
- require "matchi/be_an_instance_of"
59
- require "matchi/be_within"
60
- require "matchi/change"
61
- require "matchi/eq"
62
- require "matchi/match"
63
- require "matchi/predicate"
64
- require "matchi/raise_exception"
65
- require "matchi/satisfy"
18
+ require_relative "matchi/be"
19
+ require_relative "matchi/be_a_kind_of"
20
+ require_relative "matchi/be_an_instance_of"
21
+ require_relative "matchi/be_within"
22
+ require_relative "matchi/change"
23
+ require_relative "matchi/eq"
24
+ require_relative "matchi/match"
25
+ require_relative "matchi/predicate"
26
+ require_relative "matchi/raise_exception"
27
+ require_relative "matchi/satisfy"
metadata CHANGED
@@ -1,22 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: matchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.0
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-01-05 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description: 'Matchi is a framework-agnostic Ruby library that provides a comprehensive
14
- set of expectation matchers for elegant and secure testing. Its design focuses on
15
- simplicity, security, and extensibility, making it easy to integrate with any testing
16
- framework. The library offers a rich collection of built-in matchers for common
17
- testing scenarios while maintaining a clear, consistent API that follows Ruby best
18
- practices. With minimal setup required and support for custom matchers, Matchi enables
19
- developers to write more reliable and maintainable tests.
12
+ description: 'Matchi provides small, explicit expectation matchers for any Ruby testing
13
+ framework. The expected value always controls the comparison, so the object under
14
+ test cannot forge a match.
20
15
 
21
16
  '
22
17
  email: contact@cyril.email
@@ -31,24 +26,28 @@ files:
31
26
  - lib/matchi/be_a_kind_of.rb
32
27
  - lib/matchi/be_an_instance_of.rb
33
28
  - lib/matchi/be_within.rb
29
+ - lib/matchi/be_within/of.rb
34
30
  - lib/matchi/change.rb
35
- - lib/matchi/change/by.rb
36
- - lib/matchi/change/by_at_least.rb
37
- - lib/matchi/change/by_at_most.rb
31
+ - lib/matchi/change/delta.rb
38
32
  - lib/matchi/change/from.rb
39
- - lib/matchi/change/from/to.rb
33
+ - lib/matchi/change/from_to.rb
40
34
  - lib/matchi/change/to.rb
35
+ - lib/matchi/constant.rb
41
36
  - lib/matchi/eq.rb
42
37
  - lib/matchi/match.rb
38
+ - lib/matchi/number.rb
43
39
  - lib/matchi/predicate.rb
44
40
  - lib/matchi/raise_exception.rb
45
41
  - lib/matchi/satisfy.rb
46
- homepage: https://github.com/fixrb/matchi
42
+ homepage: https://fixrb.dev/gems/matchi/
47
43
  licenses:
48
44
  - MIT
49
45
  metadata:
46
+ bug_tracker_uri: https://github.com/fixrb/matchi/issues
47
+ changelog_uri: https://github.com/fixrb/matchi#changes-in-60
48
+ documentation_uri: https://rubydoc.info/gems/matchi
50
49
  rubygems_mfa_required: 'true'
51
- post_install_message:
50
+ source_code_uri: https://github.com/fixrb/matchi
52
51
  rdoc_options: []
53
52
  require_paths:
54
53
  - lib
@@ -56,15 +55,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
56
55
  requirements:
57
56
  - - ">="
58
57
  - !ruby/object:Gem::Version
59
- version: 3.1.0
58
+ version: 3.3.0
60
59
  required_rubygems_version: !ruby/object:Gem::Requirement
61
60
  requirements:
62
61
  - - ">="
63
62
  - !ruby/object:Gem::Version
64
63
  version: '0'
65
64
  requirements: []
66
- rubygems_version: 3.3.27
67
- signing_key:
65
+ rubygems_version: 4.0.23
68
66
  specification_version: 4
69
- summary: "Framework-agnostic matchers for secure, elegant Ruby testing \U0001F939"
67
+ summary: Framework-agnostic matchers for secure Ruby testing
70
68
  test_files: []
@@ -1,137 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Matchi
4
- class Change
5
- # Exact delta matcher that verifies precise numeric changes in object state.
6
- #
7
- # This matcher ensures that a numeric value changes by exactly the specified amount
8
- # after executing a block of code. It's particularly useful for testing operations
9
- # that should produce precise, predictable changes in numeric attributes, such as
10
- # counters, quantities, or calculated values.
11
- #
12
- # @example Testing collection size changes
13
- # array = []
14
- # matcher = Matchi::Change::By.new(2) { array.size }
15
- # matcher.match? { array.concat([1, 2]) } # => true
16
- # matcher.match? { array.push(3) } # => false # Changed by 1
17
- # matcher.match? { array.push(3, 4, 5) } # => false # Changed by 3
18
- #
19
- # @example Verifying numeric calculations
20
- # counter = 100
21
- # matcher = Matchi::Change::By.new(-10) { counter }
22
- # matcher.match? { counter -= 10 } # => true
23
- # matcher.match? { counter -= 15 } # => false # Changed too much
24
- #
25
- # @example Working with custom numeric attributes
26
- # class Account
27
- # attr_reader :balance
28
- #
29
- # def initialize(initial_balance)
30
- # @balance = initial_balance
31
- # end
32
- #
33
- # def deposit(amount)
34
- # @balance += amount
35
- # end
36
- #
37
- # def withdraw(amount)
38
- # @balance -= amount
39
- # end
40
- # end
41
- #
42
- # account = Account.new(1000)
43
- # matcher = Matchi::Change::By.new(50) { account.balance }
44
- # matcher.match? { account.deposit(50) } # => true
45
- # matcher.match? { account.deposit(75) } # => false # Changed by 75
46
- #
47
- # @example Handling floating point values
48
- # temperature = 20.0
49
- # matcher = Matchi::Change::By.new(1.5) { temperature }
50
- # matcher.match? { temperature += 1.5 } # => true
51
- # matcher.match? { temperature += 1.6 } # => false # Not exact match
52
- #
53
- # @note This matcher checks for exact changes, use ByAtLeast or ByAtMost for
54
- # more flexible delta comparisons
55
- #
56
- # @see Matchi::Change::ByAtLeast For minimum change validation
57
- # @see Matchi::Change::ByAtMost For maximum change validation
58
- # @see Matchi::Change::From::To For exact value transition validation
59
- class By
60
- # Initialize the matcher with an expected delta and a state block.
61
- #
62
- # @api public
63
- #
64
- # @param expected [Numeric] The exact amount by which the value should change
65
- # @param state [Proc] Block that retrieves the current value
66
- #
67
- # @raise [ArgumentError] if expected is not a Numeric
68
- # @raise [ArgumentError] if no state block is provided
69
- #
70
- # @return [By] a new instance of the matcher
71
- #
72
- # @example With positive delta
73
- # By.new(5) { counter.value }
74
- #
75
- # @example With negative delta
76
- # By.new(-3) { stock_level.quantity }
77
- #
78
- # @example With floating point delta
79
- # By.new(0.5) { temperature.celsius }
80
- def initialize(expected, &state)
81
- raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
82
- raise ::ArgumentError, "a block must be provided" unless block_given?
83
-
84
- @expected = expected
85
- @state = state
86
- end
87
-
88
- # Verifies that the value changes by exactly the expected amount.
89
- #
90
- # This method compares the value before and after executing the provided block,
91
- # ensuring that the difference matches the expected delta exactly. It's useful
92
- # for cases where precision is important and approximate changes are not acceptable.
93
- #
94
- # @api public
95
- #
96
- # @yield [] Block that should cause the state change
97
- # @yieldreturn [Object] The result of the block (not used)
98
- #
99
- # @return [Boolean] true if the value changed by exactly the expected amount
100
- #
101
- # @raise [ArgumentError] if no block is provided
102
- #
103
- # @example Basic usage
104
- # counter = 0
105
- # matcher = By.new(5) { counter }
106
- # matcher.match? { counter += 5 } # => true
107
- #
108
- # @example Failed match
109
- # items = []
110
- # matcher = By.new(2) { items.size }
111
- # matcher.match? { items.push(1) } # => false # Changed by 1
112
- def match?
113
- raise ::ArgumentError, "a block must be provided" unless block_given?
114
-
115
- value_before = @state.call
116
- yield
117
- value_after = @state.call
118
-
119
- @expected == (value_after - value_before)
120
- end
121
-
122
- # Returns a human-readable description of the matcher.
123
- #
124
- # @api public
125
- #
126
- # @return [String] A string describing what this matcher verifies
127
- #
128
- # @example
129
- # By.new(5).to_s # => "change by 5"
130
- # By.new(-3).to_s # => "change by -3"
131
- # By.new(2.5).to_s # => "change by 2.5"
132
- def to_s
133
- "change by #{@expected.inspect}"
134
- end
135
- end
136
- end
137
- end