matchi 4.0.0 → 4.1.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/README.md +76 -15
- data/lib/matchi.rb +300 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 730c0baf16a64842af123077150d70aee64e664ef6d5626dadf4730c1ee3425d
|
4
|
+
data.tar.gz: a934654b282119e28d4fb5c4079298770f86a834d58621240737f2473f067500
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 75884ba2533b155d3f1e4490f9b8f6071a95aad0fca637dcdb0eee2bf60e1a3dc6bd7cab35ab93a3a7c18b680b9d789ee71bfbd7d524e6ca5ea8573520a3deec
|
7
|
+
data.tar.gz: d421e42619fc45d1f1aab95ccde2a24d6a1226f4163a4afbde02fc8e693343997fa5a9a0081d72734955b2b6a25b3f9f735766702860d2184c44f403e169f442
|
data/README.md
CHANGED
@@ -63,6 +63,67 @@ A **Matchi** matcher is a simple Ruby object that follows these requirements:
|
|
63
63
|
2. Optionally, it may implement:
|
64
64
|
- `to_s`: Returns a human-readable description of the match criteria
|
65
65
|
|
66
|
+
### Using Matchers
|
67
|
+
|
68
|
+
There are two main ways to use Matchi matchers:
|
69
|
+
|
70
|
+
#### 1. Direct Class Instantiation
|
71
|
+
|
72
|
+
You can create matchers directly from their classes:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
matcher = Matchi::Eq.new("foo")
|
76
|
+
matcher.match? { "foo" } # => true
|
77
|
+
|
78
|
+
matcher = Matchi::BeWithin.new(0.5).of(3.0)
|
79
|
+
matcher.match? { 3.2 } # => true
|
80
|
+
```
|
81
|
+
|
82
|
+
#### 2. Helper Methods via Module Inclusion
|
83
|
+
|
84
|
+
For a more expressive and readable syntax, you can include or extend the `Matchi` module to get access to helper methods:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# Including in a class for instance methods
|
88
|
+
class MyTestFramework
|
89
|
+
include Matchi
|
90
|
+
|
91
|
+
def test_equality
|
92
|
+
# Helper methods available as instance methods
|
93
|
+
matcher = eq("foo")
|
94
|
+
assert(matcher.match? { "foo" })
|
95
|
+
|
96
|
+
# Method chaining works too
|
97
|
+
assert(change(@array, :length).by(1).match? { @array << 1 })
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Extending a class for class methods
|
102
|
+
class MyAssertions
|
103
|
+
extend Matchi
|
104
|
+
|
105
|
+
def self.assert_equals(expected, actual)
|
106
|
+
eq(expected).match? { actual }
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.assert_within_range(expected, delta, actual)
|
110
|
+
be_within(delta).of(expected).match? { actual }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
Available helper methods correspond to the built-in matchers:
|
116
|
+
- `eq` / `eql` - For equivalence matching
|
117
|
+
- `be` / `equal` - For identity matching
|
118
|
+
- `be_within` - For delta comparisons
|
119
|
+
- `match` - For regular expression matching
|
120
|
+
- `change` - For state changes
|
121
|
+
- `be_true`, `be_false`, `be_nil` - For state verification
|
122
|
+
- `be_an_instance_of` - For exact type matching
|
123
|
+
- `be_a_kind_of` - For type hierarchy matching
|
124
|
+
- `satisfy` - For custom block-based matching
|
125
|
+
- Dynamic predicate matchers (`be_*` and `have_*`)
|
126
|
+
|
66
127
|
### Built-in matchers
|
67
128
|
|
68
129
|
Here is the collection of generic matchers.
|
@@ -72,15 +133,15 @@ Here is the collection of generic matchers.
|
|
72
133
|
##### `Be`
|
73
134
|
Checks for object identity using Ruby's `equal?` method.
|
74
135
|
```ruby
|
75
|
-
Matchi::Be.new(:foo).match? { :foo }
|
76
|
-
Matchi::Be.new("test").match? { "test" }
|
136
|
+
Matchi::Be.new(:foo).match? { :foo } # => true (same object)
|
137
|
+
Matchi::Be.new("test").match? { "test" } # => false (different objects)
|
77
138
|
```
|
78
139
|
|
79
140
|
##### `Eq`
|
80
141
|
Verifies object equivalence using Ruby's `eql?` method.
|
81
142
|
```ruby
|
82
|
-
Matchi::Eq.new("foo").match? { "foo" }
|
83
|
-
Matchi::Eq.new([1, 2]).match? { [1, 2] }
|
143
|
+
Matchi::Eq.new("foo").match? { "foo" } # => true (equivalent content)
|
144
|
+
Matchi::Eq.new([1, 2]).match? { [1, 2] } # => true (equivalent arrays)
|
84
145
|
```
|
85
146
|
|
86
147
|
#### Type and Class Matchers
|
@@ -113,8 +174,8 @@ Matchi::Match.new(/^foo/).match? { "barfoo" } # => false
|
|
113
174
|
##### `Satisfy`
|
114
175
|
Provides custom matching through a block.
|
115
176
|
```ruby
|
116
|
-
Matchi::Satisfy.new { |x| x
|
117
|
-
Matchi::Satisfy.new { |x| x.start_with?("test") }.match? { "test_file" }
|
177
|
+
Matchi::Satisfy.new { |x| x.positive? && x < 10 }.match? { 5 } # => true
|
178
|
+
Matchi::Satisfy.new { |x| x.start_with?("test") }.match? { "test_file" } # => true
|
118
179
|
```
|
119
180
|
|
120
181
|
#### State Change Matchers
|
@@ -125,31 +186,31 @@ Verifies state changes in objects with multiple variation methods:
|
|
125
186
|
###### Basic Change
|
126
187
|
```ruby
|
127
188
|
array = []
|
128
|
-
Matchi::Change.new(array, :length).by(2).match? { array.push(1, 2) }
|
189
|
+
Matchi::Change.new(array, :length).by(2).match? { array.push(1, 2) } # => true
|
129
190
|
```
|
130
191
|
|
131
192
|
###### Minimum Change
|
132
193
|
```ruby
|
133
194
|
counter = 0
|
134
|
-
Matchi::Change.new(counter, :to_i).by_at_least(2).match? { counter += 3 }
|
195
|
+
Matchi::Change.new(counter, :to_i).by_at_least(2).match? { counter += 3 } # => true
|
135
196
|
```
|
136
197
|
|
137
198
|
###### Maximum Change
|
138
199
|
```ruby
|
139
200
|
value = 10
|
140
|
-
Matchi::Change.new(value, :to_i).by_at_most(5).match? { value += 3 }
|
201
|
+
Matchi::Change.new(value, :to_i).by_at_most(5).match? { value += 3 } # => true
|
141
202
|
```
|
142
203
|
|
143
204
|
###### From-To Change
|
144
205
|
```ruby
|
145
206
|
string = "hello"
|
146
|
-
Matchi::Change.new(string, :upcase).from("hello").to("HELLO").match? { string.upcase! }
|
207
|
+
Matchi::Change.new(string, :upcase).from("hello").to("HELLO").match? { string.upcase! } # => true
|
147
208
|
```
|
148
209
|
|
149
210
|
###### To-Only Change
|
150
211
|
```ruby
|
151
212
|
number = 1
|
152
|
-
Matchi::Change.new(number, :to_i).to(5).match? { number = 5 }
|
213
|
+
Matchi::Change.new(number, :to_i).to(5).match? { number = 5 } # => true
|
153
214
|
```
|
154
215
|
|
155
216
|
#### Numeric Matchers
|
@@ -166,8 +227,8 @@ Matchi::BeWithin.new(5).of(100).match? { 98 } # => true
|
|
166
227
|
##### `RaiseException`
|
167
228
|
Verifies that code raises specific exceptions.
|
168
229
|
```ruby
|
169
|
-
Matchi::RaiseException.new(ArgumentError).match? { raise ArgumentError }
|
170
|
-
Matchi::RaiseException.new(NameError).match? { undefined_variable }
|
230
|
+
Matchi::RaiseException.new(ArgumentError).match? { raise ArgumentError } # => true
|
231
|
+
Matchi::RaiseException.new(NameError).match? { undefined_variable } # => true
|
171
232
|
```
|
172
233
|
|
173
234
|
##### `Predicate`
|
@@ -181,7 +242,7 @@ Matchi::Predicate.new(:be_nil).match? { nil } # => true (calls nil?)
|
|
181
242
|
|
182
243
|
###### Using `have_` prefix
|
183
244
|
```ruby
|
184
|
-
Matchi::Predicate.new(:have_key, :foo).match? { { foo: 42 } }
|
245
|
+
Matchi::Predicate.new(:have_key, :foo).match? { { foo: 42 } } # => true (calls has_key?)
|
185
246
|
```
|
186
247
|
|
187
248
|
### Custom matchers
|
@@ -288,7 +349,7 @@ This is why Matchi's built-in matchers are implemented with this security consid
|
|
288
349
|
```ruby
|
289
350
|
# Implementation in Matchi::Eq
|
290
351
|
def match?
|
291
|
-
@expected.eql?(yield)
|
352
|
+
@expected.eql?(yield) # Expected value controls the comparison
|
292
353
|
end
|
293
354
|
```
|
294
355
|
|
data/lib/matchi.rb
CHANGED
@@ -2,8 +2,308 @@
|
|
2
2
|
|
3
3
|
# A collection of damn simple expectation matchers.
|
4
4
|
#
|
5
|
+
# The Matchi module provides two ways to create and use matchers:
|
6
|
+
#
|
7
|
+
# 1. Direct instantiation through classes:
|
8
|
+
# ```ruby
|
9
|
+
# matcher = Matchi::Eq.new("foo")
|
10
|
+
# matcher.match? { "foo" } # => true
|
11
|
+
# ```
|
12
|
+
#
|
13
|
+
# 2. Helper methods by including/extending the Matchi module:
|
14
|
+
# ```ruby
|
15
|
+
# # Via include in a class
|
16
|
+
# class MyTestFramework
|
17
|
+
# include Matchi
|
18
|
+
#
|
19
|
+
# def test_something
|
20
|
+
# # Helpers are now available as instance methods
|
21
|
+
# matcher = eq("foo")
|
22
|
+
# matcher.match? { "foo" } # => true
|
23
|
+
#
|
24
|
+
# # Multiple helpers can be chained
|
25
|
+
# change(@array, :length).by(1).match? { @array << 1 }
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# # Or via extend for direct usage
|
30
|
+
# class MyOtherFramework
|
31
|
+
# extend Matchi
|
32
|
+
#
|
33
|
+
# def self.assert_equals(expected, actual)
|
34
|
+
# eq(expected).match? { actual }
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
# ```
|
38
|
+
#
|
39
|
+
# The helper method approach provides a more readable and fluent interface,
|
40
|
+
# making tests easier to write and understand. It's particularly useful when
|
41
|
+
# integrating with testing frameworks or creating custom assertions.
|
42
|
+
#
|
43
|
+
# Available helpers include:
|
44
|
+
# - eq/eql : Equivalence matching
|
45
|
+
# - be/equal : Identity matching
|
46
|
+
# - be_within : Delta comparison
|
47
|
+
# - match : Regular expression matching
|
48
|
+
# - change : State change verification
|
49
|
+
# - be_true/be_false/be_nil : State verification
|
50
|
+
# - be_an_instance_of : Exact type matching
|
51
|
+
# - be_a_kind_of : Type hierarchy matching
|
52
|
+
# - satisfy : Custom block-based matching
|
53
|
+
# - be_* & have_* : Dynamic predicate matching
|
54
|
+
#
|
5
55
|
# @api public
|
6
56
|
module Matchi
|
57
|
+
# Equivalence matcher
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# matcher = eq("foo")
|
61
|
+
# matcher.match? { "foo" } # => true
|
62
|
+
# matcher.match? { "bar" } # => false
|
63
|
+
#
|
64
|
+
# @param expected [#eql?] An expected equivalent object.
|
65
|
+
#
|
66
|
+
# @return [#match?] An equivalence matcher.
|
67
|
+
#
|
68
|
+
# @api public
|
69
|
+
def eq(expected)
|
70
|
+
::Matchi::Eq.new(expected)
|
71
|
+
end
|
72
|
+
|
73
|
+
alias eql eq
|
74
|
+
|
75
|
+
# Identity matcher
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# object = "foo"
|
79
|
+
# matcher = be(object)
|
80
|
+
# matcher.match? { object } # => true
|
81
|
+
# matcher.match? { "foo" } # => false
|
82
|
+
#
|
83
|
+
# @param expected [#equal?] The expected identical object.
|
84
|
+
#
|
85
|
+
# @return [#match?] An identity matcher.
|
86
|
+
#
|
87
|
+
# @api public
|
88
|
+
def be(expected)
|
89
|
+
::Matchi::Be.new(expected)
|
90
|
+
end
|
91
|
+
|
92
|
+
alias equal be
|
93
|
+
|
94
|
+
# Comparisons matcher
|
95
|
+
#
|
96
|
+
# @example
|
97
|
+
# matcher = be_within(1).of(41)
|
98
|
+
# matcher.match? { 42 } # => true
|
99
|
+
# matcher.match? { 43 } # => false
|
100
|
+
#
|
101
|
+
# @param delta [Numeric] A numeric value.
|
102
|
+
#
|
103
|
+
# @return [#match?] A comparison matcher.
|
104
|
+
#
|
105
|
+
# @api public
|
106
|
+
def be_within(delta)
|
107
|
+
::Matchi::BeWithin.new(delta)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Regular expressions matcher
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# matcher = match(/^foo$/)
|
114
|
+
# matcher.match? { "foo" } # => true
|
115
|
+
# matcher.match? { "bar" } # => false
|
116
|
+
#
|
117
|
+
# @param expected [#match] A regular expression.
|
118
|
+
#
|
119
|
+
# @return [#match?] A regular expression matcher.
|
120
|
+
#
|
121
|
+
# @api public
|
122
|
+
def match(expected)
|
123
|
+
::Matchi::Match.new(expected)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Expecting errors matcher
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# matcher = raise_exception(NameError)
|
130
|
+
# matcher.match? { Boom } # => true
|
131
|
+
# matcher.match? { true } # => false
|
132
|
+
#
|
133
|
+
# @param expected [Exception, #to_s] The expected exception name.
|
134
|
+
#
|
135
|
+
# @return [#match?] An error matcher.
|
136
|
+
#
|
137
|
+
# @api public
|
138
|
+
def raise_exception(expected)
|
139
|
+
::Matchi::RaiseException.new(expected)
|
140
|
+
end
|
141
|
+
|
142
|
+
# True matcher
|
143
|
+
#
|
144
|
+
# @example
|
145
|
+
# matcher = be_true
|
146
|
+
# matcher.match? { true } # => true
|
147
|
+
# matcher.match? { false } # => false
|
148
|
+
# matcher.match? { nil } # => false
|
149
|
+
# matcher.match? { 4 } # => false
|
150
|
+
#
|
151
|
+
# @return [#match?] A `true` matcher.
|
152
|
+
#
|
153
|
+
# @api public
|
154
|
+
def be_true
|
155
|
+
be(true)
|
156
|
+
end
|
157
|
+
|
158
|
+
# False matcher
|
159
|
+
#
|
160
|
+
# @example
|
161
|
+
# matcher = be_false
|
162
|
+
# matcher.match? { false } # => true
|
163
|
+
# matcher.match? { true } # => false
|
164
|
+
# matcher.match? { nil } # => false
|
165
|
+
# matcher.match? { 4 } # => false
|
166
|
+
#
|
167
|
+
# @return [#match?] A `false` matcher.
|
168
|
+
#
|
169
|
+
# @api public
|
170
|
+
def be_false
|
171
|
+
be(false)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Nil matcher
|
175
|
+
#
|
176
|
+
# @example
|
177
|
+
# matcher = be_nil
|
178
|
+
# matcher.match? { nil } # => true
|
179
|
+
# matcher.match? { false } # => false
|
180
|
+
# matcher.match? { true } # => false
|
181
|
+
# matcher.match? { 4 } # => false
|
182
|
+
#
|
183
|
+
# @return [#match?] A `nil` matcher.
|
184
|
+
#
|
185
|
+
# @api public
|
186
|
+
def be_nil
|
187
|
+
be(nil)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Type/class matcher
|
191
|
+
#
|
192
|
+
# Verifies exact class matching (no inheritance).
|
193
|
+
#
|
194
|
+
# @example
|
195
|
+
# matcher = be_an_instance_of(String)
|
196
|
+
# matcher.match? { "foo" } # => true
|
197
|
+
# matcher.match? { 4 } # => false
|
198
|
+
#
|
199
|
+
# @param expected [Class, #to_s] The expected class name.
|
200
|
+
#
|
201
|
+
# @return [#match?] A type/class matcher.
|
202
|
+
#
|
203
|
+
# @api public
|
204
|
+
def be_an_instance_of(expected)
|
205
|
+
::Matchi::BeAnInstanceOf.new(expected)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Type/class matcher
|
209
|
+
#
|
210
|
+
# Verifies class inheritance and module inclusion.
|
211
|
+
#
|
212
|
+
# @example
|
213
|
+
# matcher = be_a_kind_of(Numeric)
|
214
|
+
# matcher.match? { 42 } # => true (Integer inherits from Numeric)
|
215
|
+
# matcher.match? { 42.0 } # => true (Float inherits from Numeric)
|
216
|
+
#
|
217
|
+
# @param expected [Class, #to_s] The expected class name.
|
218
|
+
#
|
219
|
+
# @return [#match?] A type/class matcher.
|
220
|
+
#
|
221
|
+
# @api public
|
222
|
+
def be_a_kind_of(expected)
|
223
|
+
::Matchi::BeAKindOf.new(expected)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Change matcher
|
227
|
+
#
|
228
|
+
# @example
|
229
|
+
# object = []
|
230
|
+
# matcher = change(object, :length).by(1)
|
231
|
+
# matcher.match? { object << 1 } # => true
|
232
|
+
#
|
233
|
+
# object = []
|
234
|
+
# matcher = change(object, :length).by_at_least(1)
|
235
|
+
# matcher.match? { object << 1 } # => true
|
236
|
+
#
|
237
|
+
# object = []
|
238
|
+
# matcher = change(object, :length).by_at_most(1)
|
239
|
+
# matcher.match? { object << 1 } # => true
|
240
|
+
#
|
241
|
+
# object = "foo"
|
242
|
+
# matcher = change(object, :to_s).from("foo").to("FOO")
|
243
|
+
# matcher.match? { object.upcase! } # => true
|
244
|
+
#
|
245
|
+
# object = "foo"
|
246
|
+
# matcher = change(object, :to_s).to("FOO")
|
247
|
+
# matcher.match? { object.upcase! } # => true
|
248
|
+
#
|
249
|
+
# @param object [#object_id] An object.
|
250
|
+
# @param method [Symbol] The name of a method.
|
251
|
+
#
|
252
|
+
# @return [#match?] A change matcher.
|
253
|
+
#
|
254
|
+
# @api public
|
255
|
+
def change(object, method, ...)
|
256
|
+
::Matchi::Change.new(object, method, ...)
|
257
|
+
end
|
258
|
+
|
259
|
+
# Satisfy matcher
|
260
|
+
#
|
261
|
+
# @example
|
262
|
+
# matcher = satisfy { |value| value == 42 }
|
263
|
+
# matcher.match? { 42 } # => true
|
264
|
+
#
|
265
|
+
# @yield [value] A block that defines the satisfaction criteria
|
266
|
+
# @yieldparam value The value to test
|
267
|
+
# @yieldreturn [Boolean] true if the value satisfies the criteria
|
268
|
+
#
|
269
|
+
# @return [#match?] A satisfy matcher.
|
270
|
+
#
|
271
|
+
# @api public
|
272
|
+
def satisfy(&)
|
273
|
+
::Matchi::Satisfy.new(&)
|
274
|
+
end
|
275
|
+
|
276
|
+
private
|
277
|
+
|
278
|
+
# Predicate matcher, or default method missing behavior.
|
279
|
+
#
|
280
|
+
# @example Empty predicate matcher
|
281
|
+
# matcher = be_empty
|
282
|
+
# matcher.match? { [] } # => true
|
283
|
+
# matcher.match? { [4] } # => false
|
284
|
+
def method_missing(name, ...)
|
285
|
+
return super unless predicate_matcher_name?(name)
|
286
|
+
|
287
|
+
::Matchi::Predicate.new(name, ...)
|
288
|
+
end
|
289
|
+
|
290
|
+
# :nocov:
|
291
|
+
|
292
|
+
# Hook method to return whether the obj can respond to id method or not.
|
293
|
+
def respond_to_missing?(name, include_private = false)
|
294
|
+
predicate_matcher_name?(name) || super
|
295
|
+
end
|
296
|
+
|
297
|
+
# :nocov:
|
298
|
+
|
299
|
+
# Predicate matcher name detector.
|
300
|
+
#
|
301
|
+
# @param name [Array, Symbol] The name of a potential predicate matcher.
|
302
|
+
#
|
303
|
+
# @return [Boolean] Indicates if it is a predicate matcher name or not.
|
304
|
+
def predicate_matcher_name?(name)
|
305
|
+
name.start_with?("be_", "have_") && !name.end_with?("!", "?")
|
306
|
+
end
|
7
307
|
end
|
8
308
|
|
9
309
|
Dir[File.join(File.dirname(__FILE__), "matchi", "*.rb")].each do |fname|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: matchi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2024-12-
|
10
|
+
date: 2024-12-31 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
12
|
description: "Collection of expectation matchers for Rubyists \U0001F939"
|
13
13
|
email: contact@cyril.email
|