matchi 4.1.1 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- 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 +12 -5
- 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
|