matchi 4.1.0 → 4.2.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.
- checksums.yaml +4 -4
- data/LICENSE.md +1 -1
- data/README.md +148 -219
- data/lib/matchi/be.rb +58 -16
- data/lib/matchi/be_a_kind_of.rb +87 -34
- data/lib/matchi/be_an_instance_of.rb +74 -62
- data/lib/matchi/be_within.rb +125 -14
- data/lib/matchi/change/by.rb +99 -22
- data/lib/matchi/change/by_at_least.rb +118 -21
- data/lib/matchi/change/by_at_most.rb +132 -22
- data/lib/matchi/change/from/to.rb +92 -25
- data/lib/matchi/change/from.rb +31 -1
- data/lib/matchi/change/to.rb +72 -23
- data/lib/matchi/change.rb +119 -45
- data/lib/matchi/eq.rb +58 -16
- data/lib/matchi/match.rb +56 -16
- data/lib/matchi/predicate.rb +122 -33
- data/lib/matchi/raise_exception.rb +89 -22
- data/lib/matchi/satisfy.rb +87 -16
- data/lib/matchi.rb +51 -297
- metadata +17 -7
- data/lib/matchi/be_within/of.rb +0 -51
@@ -2,20 +2,102 @@
|
|
2
2
|
|
3
3
|
module Matchi
|
4
4
|
class Change
|
5
|
-
#
|
5
|
+
# Minimum delta matcher that verifies numeric changes meet or exceed a threshold.
|
6
|
+
#
|
7
|
+
# This matcher ensures that a numeric value changes by at least the specified amount
|
8
|
+
# after executing a block of code. It's particularly useful when testing operations
|
9
|
+
# where you want to ensure a minimum change occurs but larger changes are acceptable,
|
10
|
+
# such as performance improvements, resource allocation, or progressive counters.
|
11
|
+
#
|
12
|
+
# @example Testing collection growth
|
13
|
+
# items = []
|
14
|
+
# matcher = Matchi::Change::ByAtLeast.new(2) { items.size }
|
15
|
+
# matcher.match? { items.push(1, 2) } # => true # Changed by exactly 2
|
16
|
+
# matcher.match? { items.push(1, 2, 3) } # => true # Changed by more than 2
|
17
|
+
# matcher.match? { items.push(1) } # => false # Changed by less than 2
|
18
|
+
#
|
19
|
+
# @example Verifying performance improvements
|
20
|
+
# class Benchmark
|
21
|
+
# def initialize
|
22
|
+
# @score = 100
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# def optimize!
|
26
|
+
# @score += rand(20..30) # Improvement varies
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# def score
|
30
|
+
# @score
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# benchmark = Benchmark.new
|
35
|
+
# matcher = Matchi::Change::ByAtLeast.new(20) { benchmark.score }
|
36
|
+
# matcher.match? { benchmark.optimize! } # => true # Any improvement >= 20 passes
|
37
|
+
#
|
38
|
+
# @example Resource allocation
|
39
|
+
# class Pool
|
40
|
+
# def initialize
|
41
|
+
# @capacity = 1000
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# def allocate(minimum, maximum)
|
45
|
+
# actual = rand(minimum..maximum)
|
46
|
+
# @capacity -= actual
|
47
|
+
# actual
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# def available
|
51
|
+
# @capacity
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# pool = Pool.new
|
56
|
+
# matcher = Matchi::Change::ByAtLeast.new(50) { -pool.available }
|
57
|
+
# matcher.match? { pool.allocate(50, 100) } # => true # Allocates at least 50
|
58
|
+
#
|
59
|
+
# @example Price threshold monitoring
|
60
|
+
# class Stock
|
61
|
+
# attr_reader :price
|
62
|
+
#
|
63
|
+
# def initialize(price)
|
64
|
+
# @price = price
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# def fluctuate!
|
68
|
+
# @price += rand(-10.0..20.0)
|
69
|
+
# end
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# stock = Stock.new(100.0)
|
73
|
+
# matcher = Matchi::Change::ByAtLeast.new(10.0) { stock.price }
|
74
|
+
# matcher.match? { stock.fluctuate! } # => true if price rises by 10.0 or more
|
75
|
+
#
|
76
|
+
# @note This matcher verifies minimum changes only. For exact changes, use By,
|
77
|
+
# and for maximum changes, use ByAtMost.
|
78
|
+
#
|
79
|
+
# @see Matchi::Change::By For exact change validation
|
80
|
+
# @see Matchi::Change::ByAtMost For maximum change validation
|
81
|
+
# @see Matchi::Change::To For final value validation
|
6
82
|
class ByAtLeast
|
7
|
-
# Initialize the matcher with
|
83
|
+
# Initialize the matcher with a minimum expected change and a state block.
|
8
84
|
#
|
9
|
-
# @
|
10
|
-
#
|
85
|
+
# @api public
|
86
|
+
#
|
87
|
+
# @param expected [Numeric] The minimum amount by which the value should change
|
88
|
+
# @param state [Proc] Block that retrieves the current value
|
89
|
+
#
|
90
|
+
# @raise [ArgumentError] if expected is not a Numeric
|
91
|
+
# @raise [ArgumentError] if expected is negative
|
92
|
+
# @raise [ArgumentError] if no state block is provided
|
11
93
|
#
|
12
|
-
#
|
94
|
+
# @return [ByAtLeast] a new instance of the matcher
|
13
95
|
#
|
14
|
-
#
|
96
|
+
# @example With integer minimum
|
97
|
+
# ByAtLeast.new(5) { counter.value }
|
15
98
|
#
|
16
|
-
# @
|
17
|
-
#
|
18
|
-
# state of the object.
|
99
|
+
# @example With floating point minimum
|
100
|
+
# ByAtLeast.new(0.5) { temperature.celsius }
|
19
101
|
def initialize(expected, &state)
|
20
102
|
raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
|
21
103
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
@@ -25,21 +107,30 @@ module Matchi
|
|
25
107
|
@state = state
|
26
108
|
end
|
27
109
|
|
28
|
-
#
|
29
|
-
# before and after the code execution.
|
110
|
+
# Checks if the value changes by at least the expected amount.
|
30
111
|
#
|
31
|
-
#
|
32
|
-
#
|
112
|
+
# This method compares the value before and after executing the provided block,
|
113
|
+
# ensuring that the difference is greater than or equal to the expected minimum.
|
114
|
+
# This is useful when you want to verify that a change meets a minimum threshold.
|
115
|
+
#
|
116
|
+
# @api public
|
33
117
|
#
|
34
|
-
#
|
118
|
+
# @yield [] Block that should cause the state change
|
119
|
+
# @yieldreturn [Object] The result of the block (not used)
|
35
120
|
#
|
36
|
-
#
|
37
|
-
# matcher.match? { object << "foo" } # => true
|
121
|
+
# @return [Boolean] true if the value changed by at least the expected amount
|
38
122
|
#
|
39
|
-
# @
|
123
|
+
# @raise [ArgumentError] if no block is provided
|
40
124
|
#
|
41
|
-
# @
|
42
|
-
#
|
125
|
+
# @example Basic usage
|
126
|
+
# counter = 0
|
127
|
+
# matcher = ByAtLeast.new(5) { counter }
|
128
|
+
# matcher.match? { counter += 7 } # => true # Changed by more than minimum
|
129
|
+
#
|
130
|
+
# @example Edge case - exact minimum
|
131
|
+
# items = []
|
132
|
+
# matcher = ByAtLeast.new(2) { items.size }
|
133
|
+
# matcher.match? { items.push(1, 2) } # => true # Changed by exactly minimum
|
43
134
|
def match?
|
44
135
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
45
136
|
|
@@ -50,9 +141,15 @@ module Matchi
|
|
50
141
|
@expected <= (value_after - value_before)
|
51
142
|
end
|
52
143
|
|
53
|
-
# Returns a
|
144
|
+
# Returns a human-readable description of the matcher.
|
145
|
+
#
|
146
|
+
# @api public
|
54
147
|
#
|
55
|
-
# @return [String]
|
148
|
+
# @return [String] A string describing what this matcher verifies
|
149
|
+
#
|
150
|
+
# @example
|
151
|
+
# ByAtLeast.new(5).to_s # => "change by at least 5"
|
152
|
+
# ByAtLeast.new(2.5).to_s # => "change by at least 2.5"
|
56
153
|
def to_s
|
57
154
|
"change by at least #{@expected.inspect}"
|
58
155
|
end
|
@@ -2,44 +2,148 @@
|
|
2
2
|
|
3
3
|
module Matchi
|
4
4
|
class Change
|
5
|
-
#
|
5
|
+
# Maximum delta matcher that verifies numeric changes don't exceed a limit.
|
6
|
+
#
|
7
|
+
# This matcher ensures that a numeric value changes by no more than the specified amount
|
8
|
+
# after executing a block of code. It's particularly useful when testing operations
|
9
|
+
# where you want to enforce an upper bound on changes, such as rate limiting,
|
10
|
+
# resource consumption, or controlled increments.
|
11
|
+
#
|
12
|
+
# @example Testing controlled collection growth
|
13
|
+
# items = []
|
14
|
+
# matcher = Matchi::Change::ByAtMost.new(2) { items.size }
|
15
|
+
# matcher.match? { items.push(1) } # => true # Changed by less than limit
|
16
|
+
# matcher.match? { items.push(1, 2) } # => true # Changed by exactly limit
|
17
|
+
# matcher.match? { items.push(1, 2, 3) } # => false # Changed by more than limit
|
18
|
+
#
|
19
|
+
# @example Rate limiting
|
20
|
+
# class RateLimiter
|
21
|
+
# def initialize
|
22
|
+
# @requests = 0
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# def process_batch(items)
|
26
|
+
# items.each do |item|
|
27
|
+
# break if @requests >= 3 # Rate limit
|
28
|
+
# process_item(item)
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# private
|
33
|
+
#
|
34
|
+
# def process_item(item)
|
35
|
+
# @requests += 1
|
36
|
+
# # Processing logic...
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# def requests
|
40
|
+
# @requests
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# limiter = RateLimiter.new
|
45
|
+
# matcher = Matchi::Change::ByAtMost.new(3) { limiter.requests }
|
46
|
+
# matcher.match? { limiter.process_batch([1, 2, 3, 4, 5]) } # => true
|
47
|
+
#
|
48
|
+
# @example Resource consumption
|
49
|
+
# class ResourcePool
|
50
|
+
# attr_reader :used
|
51
|
+
#
|
52
|
+
# def initialize
|
53
|
+
# @used = 0
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# def allocate(requested)
|
57
|
+
# available = 5 - @used # Maximum pool size is 5
|
58
|
+
# granted = [requested, available].min
|
59
|
+
# @used += granted
|
60
|
+
# granted
|
61
|
+
# end
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# pool = ResourcePool.new
|
65
|
+
# matcher = Matchi::Change::ByAtMost.new(2) { pool.used }
|
66
|
+
# matcher.match? { pool.allocate(2) } # => true
|
67
|
+
# matcher.match? { pool.allocate(3) } # => false
|
68
|
+
#
|
69
|
+
# @example Score adjustments
|
70
|
+
# class GameScore
|
71
|
+
# attr_reader :value
|
72
|
+
#
|
73
|
+
# def initialize
|
74
|
+
# @value = 100
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# def apply_penalty(amount)
|
78
|
+
# max_penalty = 10
|
79
|
+
# actual_penalty = [amount, max_penalty].min
|
80
|
+
# @value -= actual_penalty
|
81
|
+
# end
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# score = GameScore.new
|
85
|
+
# matcher = Matchi::Change::ByAtMost.new(10) { -score.value }
|
86
|
+
# matcher.match? { score.apply_penalty(5) } # => true # Small penalty
|
87
|
+
# matcher.match? { score.apply_penalty(15) } # => true # Limited to max
|
88
|
+
#
|
89
|
+
# @note This matcher verifies maximum changes only. For exact changes, use By,
|
90
|
+
# and for minimum changes, use ByAtLeast.
|
91
|
+
#
|
92
|
+
# @see Matchi::Change::By For exact change validation
|
93
|
+
# @see Matchi::Change::ByAtLeast For minimum change validation
|
94
|
+
# @see Matchi::Change::To For final value validation
|
6
95
|
class ByAtMost
|
7
|
-
# Initialize the matcher with
|
96
|
+
# Initialize the matcher with a maximum allowed change and a state block.
|
8
97
|
#
|
9
|
-
# @
|
10
|
-
#
|
98
|
+
# @api public
|
99
|
+
#
|
100
|
+
# @param expected [Numeric] The maximum amount by which the value should change
|
101
|
+
# @param state [Proc] Block that retrieves the current value
|
102
|
+
#
|
103
|
+
# @raise [ArgumentError] if expected is not a Numeric
|
104
|
+
# @raise [ArgumentError] if expected is negative
|
105
|
+
# @raise [ArgumentError] if no state block is provided
|
11
106
|
#
|
12
|
-
#
|
107
|
+
# @return [ByAtMost] a new instance of the matcher
|
13
108
|
#
|
14
|
-
#
|
109
|
+
# @example With integer maximum
|
110
|
+
# ByAtMost.new(5) { counter.value }
|
15
111
|
#
|
16
|
-
# @
|
17
|
-
#
|
18
|
-
# state of the object.
|
112
|
+
# @example With floating point maximum
|
113
|
+
# ByAtMost.new(0.5) { temperature.celsius }
|
19
114
|
def initialize(expected, &state)
|
20
115
|
raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
|
21
116
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
22
117
|
raise ::ArgumentError, "expected must be non-negative" if expected.negative?
|
23
118
|
|
24
119
|
@expected = expected
|
25
|
-
@state
|
120
|
+
@state = state
|
26
121
|
end
|
27
122
|
|
28
|
-
#
|
29
|
-
# before and after the code execution.
|
123
|
+
# Checks if the value changes by no more than the expected amount.
|
30
124
|
#
|
31
|
-
#
|
32
|
-
#
|
125
|
+
# This method compares the value before and after executing the provided block,
|
126
|
+
# ensuring that the absolute difference is less than or equal to the expected maximum.
|
127
|
+
# This is useful for enforcing upper bounds on state changes.
|
128
|
+
#
|
129
|
+
# @api public
|
33
130
|
#
|
34
|
-
#
|
131
|
+
# @yield [] Block that should cause the state change
|
132
|
+
# @yieldreturn [Object] The result of the block (not used)
|
35
133
|
#
|
36
|
-
#
|
37
|
-
# matcher.match? { object << "foo" } # => true
|
134
|
+
# @return [Boolean] true if the value changed by at most the expected amount
|
38
135
|
#
|
39
|
-
# @
|
136
|
+
# @raise [ArgumentError] if no block is provided
|
40
137
|
#
|
41
|
-
# @
|
42
|
-
#
|
138
|
+
# @example Basic usage with growth
|
139
|
+
# users = []
|
140
|
+
# matcher = ByAtMost.new(2) { users.size }
|
141
|
+
# matcher.match? { users.push('alice') } # => true
|
142
|
+
#
|
143
|
+
# @example With negative changes
|
144
|
+
# stock = 10
|
145
|
+
# matcher = ByAtMost.new(3) { stock }
|
146
|
+
# matcher.match? { stock -= 2 } # => true
|
43
147
|
def match?
|
44
148
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
45
149
|
|
@@ -50,9 +154,15 @@ module Matchi
|
|
50
154
|
@expected >= (value_after - value_before)
|
51
155
|
end
|
52
156
|
|
53
|
-
# Returns a
|
157
|
+
# Returns a human-readable description of the matcher.
|
158
|
+
#
|
159
|
+
# @api public
|
54
160
|
#
|
55
|
-
# @return [String]
|
161
|
+
# @return [String] A string describing what this matcher verifies
|
162
|
+
#
|
163
|
+
# @example
|
164
|
+
# ByAtMost.new(5).to_s # => "change by at most 5"
|
165
|
+
# ByAtMost.new(2.5).to_s # => "change by at most 2.5"
|
56
166
|
def to_s
|
57
167
|
"change by at most #{@expected.inspect}"
|
58
168
|
end
|
@@ -3,44 +3,105 @@
|
|
3
3
|
module Matchi
|
4
4
|
class Change
|
5
5
|
class From
|
6
|
-
#
|
6
|
+
# Value transition matcher that verifies both initial and final states of an operation.
|
7
|
+
#
|
8
|
+
# This matcher ensures that a value not only changes to an expected final state but also
|
9
|
+
# starts from a specific initial state. This is particularly useful when testing state
|
10
|
+
# transitions where both the starting and ending conditions are important, such as in
|
11
|
+
# workflow systems, state machines, or data transformations.
|
12
|
+
#
|
13
|
+
# @example Basic string transformation
|
14
|
+
# text = "hello"
|
15
|
+
# matcher = Matchi::Change::From::To.new("hello", "HELLO") { text.to_s }
|
16
|
+
# matcher.match? { text.upcase! } # => true
|
17
|
+
# matcher.match? { text.reverse! } # => false # Wrong final state
|
18
|
+
#
|
19
|
+
# text = "other"
|
20
|
+
# matcher.match? { text.upcase! } # => false # Wrong initial state
|
21
|
+
#
|
22
|
+
# @example State machine transitions
|
23
|
+
# class Order
|
24
|
+
# attr_accessor :status
|
25
|
+
# def initialize(status)
|
26
|
+
# @status = status
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# order = Order.new(:pending)
|
31
|
+
# matcher = Matchi::Change::From::To.new(:pending, :shipped) { order.status }
|
32
|
+
# matcher.match? { order.status = :shipped } # => true
|
33
|
+
#
|
34
|
+
# @example Complex object transformations
|
35
|
+
# class User
|
36
|
+
# attr_accessor :permissions
|
37
|
+
# def initialize
|
38
|
+
# @permissions = [:read]
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# def promote!
|
42
|
+
# @permissions += [:write, :delete]
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# user = User.new
|
47
|
+
# matcher = Matchi::Change::From::To.new(
|
48
|
+
# [:read],
|
49
|
+
# [:read, :write, :delete]
|
50
|
+
# ) { user.permissions }
|
51
|
+
# matcher.match? { user.promote! } # => true
|
52
|
+
#
|
53
|
+
# @see Matchi::Change::To For checking only the final state
|
54
|
+
# @see Matchi::Change::By For checking numeric changes
|
7
55
|
class To
|
8
|
-
# Initialize the matcher with
|
56
|
+
# Initialize the matcher with expected initial and final values.
|
9
57
|
#
|
10
|
-
# @
|
11
|
-
#
|
58
|
+
# @api public
|
59
|
+
#
|
60
|
+
# @param expected_init [#==] The expected initial value
|
61
|
+
# @param expected_new_value [#==] The expected final value
|
62
|
+
# @param state [Proc] Block that retrieves the current value
|
63
|
+
#
|
64
|
+
# @raise [ArgumentError] if no state block is provided
|
12
65
|
#
|
13
|
-
#
|
66
|
+
# @return [To] a new instance of the matcher
|
14
67
|
#
|
15
|
-
#
|
68
|
+
# @example With simple value
|
69
|
+
# To.new("draft", "published") { document.status }
|
16
70
|
#
|
17
|
-
# @
|
18
|
-
#
|
19
|
-
# @param state [Proc] A block of code to execute to
|
20
|
-
# get the state of the object.
|
71
|
+
# @example With complex state
|
72
|
+
# To.new([:user], [:user, :admin]) { account.roles }
|
21
73
|
def initialize(expected_init, expected_new_value, &state)
|
22
74
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
23
75
|
|
24
|
-
@expected_init
|
25
|
-
@expected
|
26
|
-
@state
|
76
|
+
@expected_init = expected_init
|
77
|
+
@expected = expected_new_value
|
78
|
+
@state = state
|
27
79
|
end
|
28
80
|
|
29
|
-
#
|
30
|
-
# before and after the code execution.
|
81
|
+
# Verifies both initial and final states during a transition.
|
31
82
|
#
|
32
|
-
#
|
33
|
-
#
|
83
|
+
# This method first checks if the initial state matches the expected value,
|
84
|
+
# then executes the provided block and verifies the final state. The match
|
85
|
+
# fails if either the initial or final state doesn't match expectations.
|
86
|
+
#
|
87
|
+
# @api public
|
34
88
|
#
|
35
|
-
#
|
89
|
+
# @yield [] Block that should cause the state transition
|
90
|
+
# @yieldreturn [Object] The result of the block (not used)
|
36
91
|
#
|
37
|
-
#
|
38
|
-
# matcher.match? { object.upcase! } # => true
|
92
|
+
# @return [Boolean] true if both initial and final states match expectations
|
39
93
|
#
|
40
|
-
# @
|
94
|
+
# @raise [ArgumentError] if no block is provided
|
41
95
|
#
|
42
|
-
# @
|
43
|
-
#
|
96
|
+
# @example Basic usage
|
97
|
+
# text = "hello"
|
98
|
+
# matcher = To.new("hello", "HELLO") { text }
|
99
|
+
# matcher.match? { text.upcase! } # => true
|
100
|
+
#
|
101
|
+
# @example Failed initial state
|
102
|
+
# text = "wrong"
|
103
|
+
# matcher = To.new("hello", "HELLO") { text }
|
104
|
+
# matcher.match? { text.upcase! } # => false
|
44
105
|
def match?
|
45
106
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
46
107
|
|
@@ -53,9 +114,15 @@ module Matchi
|
|
53
114
|
@expected == value_after
|
54
115
|
end
|
55
116
|
|
56
|
-
# Returns a
|
117
|
+
# Returns a human-readable description of the matcher.
|
118
|
+
#
|
119
|
+
# @api public
|
57
120
|
#
|
58
|
-
# @return [String]
|
121
|
+
# @return [String] A string describing what this matcher verifies
|
122
|
+
#
|
123
|
+
# @example
|
124
|
+
# To.new("draft", "published").to_s
|
125
|
+
# # => 'change from "draft" to "published"'
|
59
126
|
def to_s
|
60
127
|
"change from #{@expected_init.inspect} to #{@expected.inspect}"
|
61
128
|
end
|
data/lib/matchi/change/from.rb
CHANGED
@@ -4,7 +4,32 @@ require_relative File.join("from", "to")
|
|
4
4
|
|
5
5
|
module Matchi
|
6
6
|
class Change
|
7
|
-
#
|
7
|
+
# Initial state wrapper for building a value transition matcher.
|
8
|
+
#
|
9
|
+
# This class acts as a wrapper that captures the expected initial state and
|
10
|
+
# provides methods to build a complete transition matcher. When combined with
|
11
|
+
# the 'to' method, it creates a matcher that verifies both the starting and
|
12
|
+
# ending values of a change operation. This is useful when you need to ensure
|
13
|
+
# not only the final state but also the initial state of a value.
|
14
|
+
#
|
15
|
+
# @example Basic string transformation
|
16
|
+
# text = "hello"
|
17
|
+
# Change.new(text, :to_s).from("hello").to("HELLO").match? { text.upcase! } # => true
|
18
|
+
#
|
19
|
+
# @example Object state transition
|
20
|
+
# class User
|
21
|
+
# attr_accessor :status
|
22
|
+
# def initialize
|
23
|
+
# @status = "pending"
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# user = User.new
|
28
|
+
# Change.new(user, :status).from("pending").to("active").match? {
|
29
|
+
# user.status = "active"
|
30
|
+
# } # => true
|
31
|
+
#
|
32
|
+
# @see Matchi::Change::From::To For the complete transition matcher
|
8
33
|
class From
|
9
34
|
# Initialize the wrapper with an object and a block.
|
10
35
|
#
|
@@ -27,6 +52,11 @@ module Matchi
|
|
27
52
|
|
28
53
|
# Specifies the new value to expect.
|
29
54
|
#
|
55
|
+
# Creates a complete transition matcher that verifies both the initial
|
56
|
+
# and final states of a value. The matcher will succeed only if the
|
57
|
+
# value starts at the expected initial state and changes to the specified
|
58
|
+
# new value after executing the test block.
|
59
|
+
#
|
30
60
|
# @example
|
31
61
|
# require "matchi/change/from"
|
32
62
|
#
|
data/lib/matchi/change/to.rb
CHANGED
@@ -2,54 +2,103 @@
|
|
2
2
|
|
3
3
|
module Matchi
|
4
4
|
class Change
|
5
|
-
#
|
5
|
+
# Final state matcher that verifies if a method returns an expected value after a change.
|
6
|
+
#
|
7
|
+
# This matcher focuses on the final state of an object, verifying that a method call
|
8
|
+
# returns an expected value after executing a block of code. Unlike the full from/to
|
9
|
+
# matcher, it only cares about the end result, not the initial state.
|
10
|
+
#
|
11
|
+
# @example Basic string transformation
|
12
|
+
# text = "hello"
|
13
|
+
# matcher = Matchi::Change::To.new("HELLO") { text.to_s }
|
14
|
+
# matcher.match? { text.upcase! } # => true
|
15
|
+
# matcher.match? { text.reverse! } # => false
|
16
|
+
#
|
17
|
+
# @example Number calculations
|
18
|
+
# counter = 0
|
19
|
+
# matcher = Matchi::Change::To.new(5) { counter }
|
20
|
+
# matcher.match? { counter = 5 } # => true
|
21
|
+
# matcher.match? { counter += 1 } # => false
|
22
|
+
#
|
23
|
+
# @example With object attributes
|
24
|
+
# class User
|
25
|
+
# attr_accessor :status
|
26
|
+
# def initialize(status)
|
27
|
+
# @status = status
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# user = User.new(:pending)
|
32
|
+
# matcher = Matchi::Change::To.new(:active) { user.status }
|
33
|
+
# matcher.match? { user.status = :active } # => true
|
34
|
+
#
|
35
|
+
# @see Matchi::Change::From::To For checking both initial and final states
|
36
|
+
# @see Matchi::Change::By For checking numeric changes
|
6
37
|
class To
|
7
|
-
# Initialize the matcher with an
|
38
|
+
# Initialize the matcher with an expected new value and a state block.
|
8
39
|
#
|
9
|
-
# @
|
10
|
-
#
|
40
|
+
# @api public
|
41
|
+
#
|
42
|
+
# @param expected [#eql?] The expected final value
|
43
|
+
# @param state [Proc] Block that retrieves the value to check
|
44
|
+
#
|
45
|
+
# @raise [ArgumentError] if no state block is provided
|
11
46
|
#
|
12
|
-
#
|
47
|
+
# @return [To] a new instance of the matcher
|
13
48
|
#
|
14
|
-
#
|
49
|
+
# @example With simple value
|
50
|
+
# To.new("test") { object.value }
|
15
51
|
#
|
16
|
-
# @
|
17
|
-
#
|
18
|
-
# state of the object.
|
52
|
+
# @example With complex calculation
|
53
|
+
# To.new(100) { object.items.count }
|
19
54
|
def initialize(expected, &state)
|
20
55
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
21
56
|
|
22
57
|
@expected = expected
|
23
|
-
@state
|
58
|
+
@state = state
|
24
59
|
end
|
25
60
|
|
26
|
-
#
|
27
|
-
# before and after the code execution.
|
61
|
+
# Checks if the state block returns the expected value after executing the provided block.
|
28
62
|
#
|
29
|
-
#
|
30
|
-
#
|
63
|
+
# This method executes the provided block and then checks if the state block
|
64
|
+
# returns the expected value. It only cares about the final state, not any
|
65
|
+
# intermediate values or the initial state.
|
66
|
+
#
|
67
|
+
# @api public
|
31
68
|
#
|
32
|
-
#
|
69
|
+
# @yield [] Block that should cause the state change
|
70
|
+
# @yieldreturn [Object] The result of the block (not used)
|
33
71
|
#
|
34
|
-
#
|
35
|
-
# matcher.match? { object.upcase! } # => true
|
72
|
+
# @return [Boolean] true if the final state matches the expected value
|
36
73
|
#
|
37
|
-
# @
|
74
|
+
# @raise [ArgumentError] if no block is provided
|
38
75
|
#
|
39
|
-
# @
|
40
|
-
#
|
76
|
+
# @example Basic usage
|
77
|
+
# text = "hello"
|
78
|
+
# matcher = To.new("HELLO") { text.to_s }
|
79
|
+
# matcher.match? { text.upcase! } # => true
|
80
|
+
#
|
81
|
+
# @example With method chaining
|
82
|
+
# array = [1, 2, 3]
|
83
|
+
# matcher = To.new(6) { array.sum }
|
84
|
+
# matcher.match? { array.map! { |x| x * 2 } } # => false
|
41
85
|
def match?
|
42
86
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
43
87
|
|
44
88
|
yield
|
45
89
|
value_after = @state.call
|
46
90
|
|
47
|
-
@expected
|
91
|
+
@expected.eql?(value_after)
|
48
92
|
end
|
49
93
|
|
50
|
-
# Returns a
|
94
|
+
# Returns a human-readable description of the matcher.
|
95
|
+
#
|
96
|
+
# @api public
|
51
97
|
#
|
52
|
-
# @return [String]
|
98
|
+
# @return [String] A string describing what this matcher verifies
|
99
|
+
#
|
100
|
+
# @example
|
101
|
+
# To.new("test").to_s # => 'change to "test"'
|
53
102
|
def to_s
|
54
103
|
"change to #{@expected.inspect}"
|
55
104
|
end
|