minicrest 1.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.
- checksums.yaml +7 -0
- data/.rubocop.yml +73 -0
- data/.yardopts +8 -0
- data/AGENTS.md +128 -0
- data/CHANGELOG.md +96 -0
- data/Gemfile +14 -0
- data/LICENSE +661 -0
- data/README.md +688 -0
- data/Rakefile +100 -0
- data/lib/minicrest/all_items.rb +61 -0
- data/lib/minicrest/anything.rb +58 -0
- data/lib/minicrest/asserter.rb +399 -0
- data/lib/minicrest/assertions.rb +411 -0
- data/lib/minicrest/between.rb +62 -0
- data/lib/minicrest/blank.rb +53 -0
- data/lib/minicrest/collection_item_matcher.rb +38 -0
- data/lib/minicrest/combinators.rb +340 -0
- data/lib/minicrest/comparison_matcher.rb +57 -0
- data/lib/minicrest/contains.rb +114 -0
- data/lib/minicrest/contains_exactly.rb +66 -0
- data/lib/minicrest/descends_from.rb +64 -0
- data/lib/minicrest/empty.rb +55 -0
- data/lib/minicrest/ends_with.rb +20 -0
- data/lib/minicrest/equals.rb +197 -0
- data/lib/minicrest/has_attribute.rb +96 -0
- data/lib/minicrest/has_key.rb +79 -0
- data/lib/minicrest/has_size.rb +88 -0
- data/lib/minicrest/has_value.rb +79 -0
- data/lib/minicrest/hash_entry_matchers.rb +119 -0
- data/lib/minicrest/includes.rb +114 -0
- data/lib/minicrest/is.rb +93 -0
- data/lib/minicrest/is_close_to.rb +61 -0
- data/lib/minicrest/is_greater_than.rb +30 -0
- data/lib/minicrest/is_greater_than_or_equal_to.rb +23 -0
- data/lib/minicrest/is_in.rb +84 -0
- data/lib/minicrest/is_less_than.rb +30 -0
- data/lib/minicrest/is_less_than_or_equal_to.rb +56 -0
- data/lib/minicrest/matcher.rb +91 -0
- data/lib/minicrest/matches_pattern.rb +60 -0
- data/lib/minicrest/no_items.rb +59 -0
- data/lib/minicrest/responds_to.rb +62 -0
- data/lib/minicrest/some_items.rb +59 -0
- data/lib/minicrest/starts_with.rb +20 -0
- data/lib/minicrest/string_matcher.rb +64 -0
- data/lib/minicrest/value_matchers.rb +86 -0
- data/lib/minicrest/version.rb +5 -0
- data/lib/minicrest.rb +110 -0
- data/minicrest.gemspec +35 -0
- metadata +108 -0
data/README.md
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
# Minicrest
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ggalmazor/minicrest/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Hamcrest-style composable matchers for Minitest. Write expressive, readable assertions with detailed failure messages.
|
|
6
|
+
|
|
7
|
+
[Documentation](https://ggalmazor.com/minicrest/)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Fluent assertion API with `assert_that`
|
|
12
|
+
- Composable matchers using `&` (AND) and `|` (OR) operators
|
|
13
|
+
- Deep equality comparison for arrays and hashes with detailed diffs
|
|
14
|
+
- Reference equality testing
|
|
15
|
+
- Logical combinators: `all_of`, `none_of`, `some_of`
|
|
16
|
+
- Descriptive failure messages that pinpoint exactly what went wrong
|
|
17
|
+
- Extensible: create and register your own custom matchers
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Add to your Gemfile:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem 'minicrest'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then run:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bundle install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
Include `Minicrest::Assertions` in your test class:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require 'minicrest'
|
|
39
|
+
|
|
40
|
+
class MyTest < Minitest::Test
|
|
41
|
+
include Minicrest::Assertions
|
|
42
|
+
|
|
43
|
+
def test_basic_equality
|
|
44
|
+
assert_that(42).equals(42)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Running Tests
|
|
50
|
+
|
|
51
|
+
To run the library's own tests:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
rake test
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Core Matchers
|
|
58
|
+
|
|
59
|
+
### Value Equality
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
# Simple values
|
|
63
|
+
assert_that("hello").equals("hello")
|
|
64
|
+
|
|
65
|
+
# Arrays with deep comparison
|
|
66
|
+
assert_that([1, [2, 3]]).equals([1, [2, 3]])
|
|
67
|
+
|
|
68
|
+
# Hashes with deep comparison
|
|
69
|
+
assert_that({ user: { name: "Alice" } }).equals({ user: { name: "Alice" } })
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Reference Equality
|
|
73
|
+
|
|
74
|
+
Test that two variables point to the same object:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
obj = Object.new
|
|
78
|
+
assert_that(obj).is(obj)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Negation
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
assert_that(42).never(equals(0))
|
|
85
|
+
assert_that(nil).never(equals(false))
|
|
86
|
+
|
|
87
|
+
# Aliases for more natural flow
|
|
88
|
+
assert_that(42).is_not(0)
|
|
89
|
+
assert_that("hello").does_not(start_with("bye"))
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Placeholder Matching
|
|
93
|
+
|
|
94
|
+
Use `anything` when you don't care about a particular value:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
assert_that(some_value).is(anything)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Use `truthy` or `falsy` to match any truthy or falsy value in Ruby:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
assert_that(some_value).is(truthy)
|
|
104
|
+
assert_that(some_value).is(anything)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Type and Method Matchers
|
|
108
|
+
|
|
109
|
+
### `is_a(type)` / `descends_from(type)`
|
|
110
|
+
|
|
111
|
+
Matches if the value is an instance of the expected type (including inheritance):
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
assert_that("hello").matches(is_a(String))
|
|
115
|
+
assert_that(42).matches(descends_from(Integer))
|
|
116
|
+
assert_that([]).matches(is_a(Enumerable)) # works with modules
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `instance_of(type)`
|
|
120
|
+
|
|
121
|
+
Matches if the value is an exact instance of the expected class (no inheritance):
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
assert_that("hello").matches(instance_of(String))
|
|
125
|
+
assert_that(42).never(instance_of(Numeric)) # Integer is a Numeric, but not exactly InstanceOf Numeric
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `responds_to(*methods)`
|
|
129
|
+
|
|
130
|
+
Matches if the value responds to all specified methods:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
assert_that("hello").matches(responds_to(:upcase))
|
|
134
|
+
assert_that([]).matches(responds_to(:push, :pop))
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Value Matchers
|
|
138
|
+
|
|
139
|
+
### `nil_value`
|
|
140
|
+
|
|
141
|
+
Matches if the value is `nil`:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
assert_that(nil).matches(nil_value)
|
|
145
|
+
assert_that(42).never(nil_value)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### `truthy`
|
|
149
|
+
|
|
150
|
+
Matches if the value is considered true (anything except `nil` or `false`):
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
assert_that(true).matches(truthy)
|
|
154
|
+
assert_that(42).matches(truthy)
|
|
155
|
+
assert_that("hello").matches(truthy)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `falsy`
|
|
159
|
+
|
|
160
|
+
Matches if the value is considered false (`nil` or `false`):
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
assert_that(false).matches(falsy)
|
|
164
|
+
assert_that(nil).matches(falsy)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## String Matchers
|
|
168
|
+
|
|
169
|
+
### `starts_with(prefix)`
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
assert_that("hello world").matches(starts_with("hello"))
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `ends_with(suffix)`
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
assert_that("hello world").matches(ends_with("world"))
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### `matches_pattern(regex)`
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
assert_that("hello123").matches(matches_pattern(/\d+/))
|
|
185
|
+
assert_that("test@example.com").matches(matches_pattern(/\A[\w.]+@[\w.]+\z/))
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `blank`
|
|
189
|
+
|
|
190
|
+
Matches empty or whitespace-only strings:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
assert_that("").matches(blank)
|
|
194
|
+
assert_that(" ").matches(blank)
|
|
195
|
+
assert_that("\t\n").matches(blank)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Size and Emptiness Matchers
|
|
199
|
+
|
|
200
|
+
### `empty`
|
|
201
|
+
|
|
202
|
+
Matches empty strings, arrays, or hashes:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
assert_that("").matches(empty)
|
|
206
|
+
assert_that([]).matches(empty)
|
|
207
|
+
assert_that({}).matches(empty)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### `has_size(expected)`
|
|
211
|
+
|
|
212
|
+
Matches values with a specific size:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
assert_that("hello").matches(has_size(5))
|
|
216
|
+
assert_that([1, 2, 3]).matches(has_size(3))
|
|
217
|
+
assert_that({ a: 1, b: 2 }).matches(has_size(2))
|
|
218
|
+
|
|
219
|
+
# Can use matchers for flexible size checks
|
|
220
|
+
assert_that([1, 2, 3]).matches(has_size(is_greater_than(2)))
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Numeric Comparison Matchers
|
|
224
|
+
|
|
225
|
+
### `is_greater_than(expected)`
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
assert_that(5).matches(is_greater_than(3))
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### `is_greater_than_or_equal_to(expected)`
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
assert_that(5).matches(is_greater_than_or_equal_to(5))
|
|
235
|
+
assert_that(6).matches(is_greater_than_or_equal_to(5))
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### `is_less_than(expected)`
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
assert_that(3).matches(is_less_than(5))
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### `is_less_than_or_equal_to(expected)`
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
assert_that(5).matches(is_less_than_or_equal_to(5))
|
|
248
|
+
assert_that(4).matches(is_less_than_or_equal_to(5))
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### `between(min, max, exclusive: false)`
|
|
252
|
+
|
|
253
|
+
Matches if the value is within the range:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
assert_that(5).matches(between(1, 10))
|
|
257
|
+
assert_that(10).never(between(1, 10, exclusive: true))
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### `is_close_to(expected, delta)`
|
|
261
|
+
|
|
262
|
+
Floating-point equality with tolerance:
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
assert_that(3.14159).matches(is_close_to(3.14, 0.01))
|
|
266
|
+
assert_that(10.0).matches(is_close_to(10.5, 0.5))
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Collection Content Matchers
|
|
270
|
+
|
|
271
|
+
### `includes(*items)`
|
|
272
|
+
|
|
273
|
+
Matches if the value contains all specified items:
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
# Strings: contains substrings
|
|
277
|
+
assert_that("hello world").matches(includes("hello", "world"))
|
|
278
|
+
|
|
279
|
+
# Arrays: contains elements
|
|
280
|
+
assert_that([1, 2, 3, 4]).matches(includes(2, 4))
|
|
281
|
+
|
|
282
|
+
# Hashes: contains key-value pairs
|
|
283
|
+
assert_that({ a: 1, b: 2, c: 3 }).matches(includes(a: 1, c: 3))
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### `has_key(*keys)`
|
|
287
|
+
|
|
288
|
+
Matches if the hash contains all specified keys:
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
assert_that({ a: 1, b: 2 }).matches(has_key(:a))
|
|
292
|
+
assert_that({ a: 1, b: 2 }).matches(has_key(:a, :b))
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### `has_value(*values)`
|
|
296
|
+
|
|
297
|
+
Matches if the hash contains all specified values:
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
assert_that({ a: 1, b: 2 }).matches(has_value(1))
|
|
301
|
+
assert_that({ a: 1, b: 2 }).matches(has_value(1, 2))
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### `contains(*items)`
|
|
305
|
+
|
|
306
|
+
Matches if the collection contains exactly the specified items in any order:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
assert_that([3, 1, 2]).matches(contains(1, 2, 3))
|
|
310
|
+
assert_that({ b: 2, a: 1 }).matches(contains(a: 1, b: 2))
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### `contains_exactly(*items)`
|
|
314
|
+
|
|
315
|
+
Matches if the array contains exactly the specified items in order:
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
assert_that([1, 2, 3]).matches(contains_exactly(1, 2, 3))
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Collection Item Matchers
|
|
322
|
+
|
|
323
|
+
### `all_items(matcher)`
|
|
324
|
+
|
|
325
|
+
Matches if all items in the collection match:
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
assert_that([2, 4, 6]).matches(all_items(descends_from(Integer)))
|
|
329
|
+
assert_that([2, 4, 6]).matches(all_items(is_greater_than(0)))
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### `some_items(matcher)`
|
|
333
|
+
|
|
334
|
+
Matches if at least one item matches:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
assert_that([1, "two", 3]).matches(some_items(descends_from(String)))
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### `no_items(matcher)`
|
|
341
|
+
|
|
342
|
+
Matches if no items match:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
assert_that([1, 2, 3]).matches(no_items(descends_from(String)))
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### `all_entries(matcher)` / `some_entry(matcher)` / `no_entry(matcher)`
|
|
349
|
+
|
|
350
|
+
Similar to item matchers, but specifically for hash entries (key-value pairs):
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
# all_entries expects a matcher that works with [key, value] arrays
|
|
354
|
+
assert_that({ a: 1, b: 2 }).matches(all_entries(includes(:a, :b) | includes(1, 2)))
|
|
355
|
+
|
|
356
|
+
# You can also use a Proc for more complex entry matching
|
|
357
|
+
assert_that({ a: 1, b: 2 }).matches(some_entry(->(entry) { entry[1] > 1 }))
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Membership Matcher
|
|
361
|
+
|
|
362
|
+
### `is_in(collection)`
|
|
363
|
+
|
|
364
|
+
Matches if the value is present in the collection:
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
assert_that(2).matches(is_in([1, 2, 3]))
|
|
368
|
+
assert_that(:a).matches(is_in({ a: 1, b: 2 })) # checks keys
|
|
369
|
+
assert_that("el").matches(is_in("hello")) # substring
|
|
370
|
+
assert_that(5).matches(is_in(1..10)) # ranges
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Object Attribute Matcher
|
|
374
|
+
|
|
375
|
+
### `has_attribute(name, matcher = nil)`
|
|
376
|
+
|
|
377
|
+
Matches if the object has the attribute, optionally checking its value:
|
|
378
|
+
|
|
379
|
+
```ruby
|
|
380
|
+
User = Struct.new(:name, :age)
|
|
381
|
+
|
|
382
|
+
user = User.new("Alice", 30)
|
|
383
|
+
|
|
384
|
+
assert_that(user).matches(has_attribute(:name))
|
|
385
|
+
assert_that(user).matches(has_attribute(:name, equals("Alice")))
|
|
386
|
+
assert_that(user).matches(has_attribute(:age, is_greater_than(18)))
|
|
387
|
+
|
|
388
|
+
# Also works with hashes
|
|
389
|
+
assert_that({ name: "Bob" }).matches(has_attribute(:name, equals("Bob")))
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Error Assertions
|
|
393
|
+
|
|
394
|
+
### `raises_error`
|
|
395
|
+
|
|
396
|
+
Assert that a block raises an error:
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
# Any error
|
|
400
|
+
assert_that { raise "boom" }.raises_error
|
|
401
|
+
|
|
402
|
+
# Specific error class
|
|
403
|
+
assert_that { raise ArgumentError, "bad" }.raises_error(ArgumentError)
|
|
404
|
+
|
|
405
|
+
# Error class with message matcher
|
|
406
|
+
assert_that { raise ArgumentError, "bad input" }.raises_error(ArgumentError, includes("bad"))
|
|
407
|
+
assert_that { raise ArgumentError, "code: 123" }.raises_error(ArgumentError, matches_pattern(/\d+/))
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### `raises_nothing`
|
|
411
|
+
|
|
412
|
+
Assert that a block does not raise:
|
|
413
|
+
|
|
414
|
+
```ruby
|
|
415
|
+
assert_that { safe_operation }.raises_nothing
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Combining Matchers
|
|
419
|
+
|
|
420
|
+
Use `|` for OR and `&` for AND:
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
# Either/or
|
|
424
|
+
assert_that(status).matches(equals(:success) | equals(:pending))
|
|
425
|
+
|
|
426
|
+
# Both conditions
|
|
427
|
+
assert_that(value).matches(is_greater_than(0) & is_less_than(100))
|
|
428
|
+
|
|
429
|
+
# Complex combinations
|
|
430
|
+
assert_that(result).matches(
|
|
431
|
+
(equals(1) | equals(2)) & never(equals(nil))
|
|
432
|
+
)
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Collection Combinators
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
# All matchers must pass
|
|
439
|
+
assert_that(5).matches(all_of(is_greater_than(0), is_less_than(10)))
|
|
440
|
+
|
|
441
|
+
# No matchers should pass
|
|
442
|
+
assert_that(10).matches(none_of(equals(5), equals(6)))
|
|
443
|
+
|
|
444
|
+
# At least one matcher must pass
|
|
445
|
+
assert_that(5).matches(some_of(equals(5), equals(999)))
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Custom Matchers
|
|
449
|
+
|
|
450
|
+
Create your own matchers by subclassing `Minicrest::Matcher`:
|
|
451
|
+
|
|
452
|
+
```ruby
|
|
453
|
+
class BePositive < Minicrest::Matcher
|
|
454
|
+
def matches?(actual)
|
|
455
|
+
actual.is_a?(Numeric) && actual > 0
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def description
|
|
459
|
+
"be positive"
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def failure_message(actual)
|
|
463
|
+
"expected #{actual.inspect} to be a positive number"
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Register the matcher
|
|
468
|
+
Minicrest.register_matcher(:be_positive) { BePositive.new }
|
|
469
|
+
|
|
470
|
+
# Use in tests
|
|
471
|
+
assert_that(5).matches(be_positive)
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Parameterized Matchers
|
|
475
|
+
|
|
476
|
+
Matchers can accept arguments:
|
|
477
|
+
|
|
478
|
+
```ruby
|
|
479
|
+
class BeDivisibleBy < Minicrest::Matcher
|
|
480
|
+
def initialize(divisor)
|
|
481
|
+
super()
|
|
482
|
+
@divisor = divisor
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def matches?(actual)
|
|
486
|
+
actual % @divisor == 0
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def description
|
|
490
|
+
"be divisible by #{@divisor}"
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
Minicrest.register_matcher(:be_divisible_by) { |n| BeDivisibleBy.new(n) }
|
|
495
|
+
|
|
496
|
+
# Use in tests
|
|
497
|
+
assert_that(10).matches(be_divisible_by(5))
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Custom Matchers with Combinators
|
|
501
|
+
|
|
502
|
+
Registered matchers automatically work with all combinators:
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
# With AND/OR operators
|
|
506
|
+
assert_that(10).matches(be_divisible_by(5) & be_divisible_by(2))
|
|
507
|
+
assert_that(10).matches(be_divisible_by(3) | be_divisible_by(5))
|
|
508
|
+
|
|
509
|
+
# With never()
|
|
510
|
+
assert_that(7).never(be_divisible_by(2))
|
|
511
|
+
|
|
512
|
+
# With all_of, some_of, none_of
|
|
513
|
+
assert_that(10).matches(all_of(be_positive, be_divisible_by(5)))
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## Failure Messages
|
|
517
|
+
|
|
518
|
+
Minicrest provides detailed failure messages with diffs:
|
|
519
|
+
|
|
520
|
+
```ruby
|
|
521
|
+
assert_that({ name: "Bob" }).equals({ name: "Alice" })
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Output:
|
|
525
|
+
|
|
526
|
+
```
|
|
527
|
+
expected {:name=>"Bob"}
|
|
528
|
+
to equal {:name=>"Alice"}
|
|
529
|
+
|
|
530
|
+
Diff:
|
|
531
|
+
key :name:
|
|
532
|
+
expected: "Alice"
|
|
533
|
+
actual: "Bob"
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Array diffs show the index:
|
|
537
|
+
|
|
538
|
+
```ruby
|
|
539
|
+
assert_that([1, 2, 4]).equals([1, 2, 3])
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Output:
|
|
543
|
+
|
|
544
|
+
```
|
|
545
|
+
expected [1, 2, 4]
|
|
546
|
+
to equal [1, 2, 3]
|
|
547
|
+
|
|
548
|
+
Diff:
|
|
549
|
+
[2]:
|
|
550
|
+
expected: 3
|
|
551
|
+
actual: 4
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
String diffs show the first difference:
|
|
555
|
+
|
|
556
|
+
```ruby
|
|
557
|
+
assert_that("hello").equals("hallo")
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
Output:
|
|
561
|
+
|
|
562
|
+
```
|
|
563
|
+
expected "hello"
|
|
564
|
+
to equal "hallo"
|
|
565
|
+
|
|
566
|
+
Diff:
|
|
567
|
+
at index 1:
|
|
568
|
+
expected: "a"
|
|
569
|
+
actual: "e"
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
## API Reference
|
|
573
|
+
|
|
574
|
+
### Assertions Module - Core
|
|
575
|
+
|
|
576
|
+
| Method | Description |
|
|
577
|
+
|--------|-------------|
|
|
578
|
+
| `assert_that(actual, message = nil)` | Entry point for value assertions |
|
|
579
|
+
| `assert_that { block }` | Entry point for block assertions |
|
|
580
|
+
| `equals(expected)` | Value equality matcher |
|
|
581
|
+
| `is(expected)` | Reference equality matcher (supports matchers) |
|
|
582
|
+
| `anything` | Matches any value |
|
|
583
|
+
| `never(matcher)` | Negates a matcher (aliases: `is_not`, `does_not`) |
|
|
584
|
+
|
|
585
|
+
### Assertions Module - Type & Method
|
|
586
|
+
|
|
587
|
+
| Method | Description |
|
|
588
|
+
|--------|-------------|
|
|
589
|
+
| `is_a(type)` | Type/class matcher (alias of `descends_from`) |
|
|
590
|
+
| `descends_from(type)` | Type/class matcher |
|
|
591
|
+
| `instance_of(type)` | Exact class matcher |
|
|
592
|
+
| `responds_to(*methods)` | Method presence matcher |
|
|
593
|
+
|
|
594
|
+
### Assertions Module - Values
|
|
595
|
+
|
|
596
|
+
| Method | Description |
|
|
597
|
+
|--------|-------------|
|
|
598
|
+
| `nil_value` | Matches `nil` |
|
|
599
|
+
| `truthy` | Matches non-nil, non-false values |
|
|
600
|
+
| `falsy` | Matches `nil` or `false` |
|
|
601
|
+
|
|
602
|
+
### Assertions Module - Strings
|
|
603
|
+
|
|
604
|
+
| Method | Description |
|
|
605
|
+
|--------|-------------|
|
|
606
|
+
| `starts_with(prefix)` | String prefix matcher |
|
|
607
|
+
| `ends_with(suffix)` | String suffix matcher |
|
|
608
|
+
| `matches_pattern(regex)` | Regex pattern matcher |
|
|
609
|
+
| `blank` | Blank string matcher |
|
|
610
|
+
|
|
611
|
+
### Assertions Module - Size & Emptiness
|
|
612
|
+
|
|
613
|
+
| Method | Description |
|
|
614
|
+
|--------|-------------|
|
|
615
|
+
| `empty` | Empty collection matcher |
|
|
616
|
+
| `has_size(expected)` | Size/length matcher |
|
|
617
|
+
|
|
618
|
+
### Assertions Module - Numeric
|
|
619
|
+
|
|
620
|
+
| Method | Description |
|
|
621
|
+
|--------|-------------|
|
|
622
|
+
| `is_greater_than(n)` | Greater than comparison |
|
|
623
|
+
| `is_greater_than_or_equal_to(n)` | Greater than or equal comparison |
|
|
624
|
+
| `is_less_than(n)` | Less than comparison |
|
|
625
|
+
| `is_less_than_or_equal_to(n)` | Less than or equal comparison |
|
|
626
|
+
| `between(min, max, exclusive: false)` | Range comparison |
|
|
627
|
+
| `is_close_to(n, delta)` | Floating-point tolerance |
|
|
628
|
+
|
|
629
|
+
### Assertions Module - Collections
|
|
630
|
+
|
|
631
|
+
| Method | Description |
|
|
632
|
+
|--------|-------------|
|
|
633
|
+
| `includes(*items)` | Partial containment |
|
|
634
|
+
| `has_key(*keys)` | Hash key presence |
|
|
635
|
+
| `has_value(*values)` | Hash value presence |
|
|
636
|
+
| `contains(*items)` | Exact items, any order |
|
|
637
|
+
| `contains_exactly(*items)` | Exact items, exact order |
|
|
638
|
+
| `all_items(matcher)` | All items match |
|
|
639
|
+
| `some_items(matcher)` | At least one matches |
|
|
640
|
+
| `no_items(matcher)` | No items match |
|
|
641
|
+
| `all_entries(matcher)` | All hash entries match |
|
|
642
|
+
| `some_entry(matcher)` | At least one entry matches |
|
|
643
|
+
| `no_entry(matcher)` | No entries match |
|
|
644
|
+
| `is_in(collection)` | Membership check |
|
|
645
|
+
|
|
646
|
+
### Assertions Module - Objects
|
|
647
|
+
|
|
648
|
+
| Method | Description |
|
|
649
|
+
|--------|-------------|
|
|
650
|
+
| `has_attribute(name, matcher = nil)` | Object attribute matcher |
|
|
651
|
+
|
|
652
|
+
### Assertions Module - Combinators
|
|
653
|
+
|
|
654
|
+
| Method | Description |
|
|
655
|
+
|--------|-------------|
|
|
656
|
+
| `all_of(*matchers)` | All matchers must match |
|
|
657
|
+
| `none_of(*matchers)` | No matcher should match |
|
|
658
|
+
| `some_of(*matchers)` | At least one must match |
|
|
659
|
+
|
|
660
|
+
### Asserter Methods
|
|
661
|
+
|
|
662
|
+
| Method | Description |
|
|
663
|
+
|--------|-------------|
|
|
664
|
+
| `.equals(expected)` | Assert value equality |
|
|
665
|
+
| `.is(expected)` | Assert reference equality (supports matchers) |
|
|
666
|
+
| `.never(matcher)` | Assert negation (aliases: `is_not`, `does_not`) |
|
|
667
|
+
| `.matches(matcher)` | Use any matcher |
|
|
668
|
+
| `.raises_error(class = nil, message_matcher = nil)` | Assert block raises |
|
|
669
|
+
| `.raises_nothing` | Assert block doesn't raise |
|
|
670
|
+
|
|
671
|
+
### Matcher Operators
|
|
672
|
+
|
|
673
|
+
| Operator | Description |
|
|
674
|
+
|----------|-------------|
|
|
675
|
+
| `&` | AND - both matchers must succeed |
|
|
676
|
+
| `\|` | OR - at least one matcher must succeed |
|
|
677
|
+
|
|
678
|
+
### Module Methods
|
|
679
|
+
|
|
680
|
+
| Method | Description |
|
|
681
|
+
|--------|-------------|
|
|
682
|
+
| `Minicrest.register_matcher(name, &block)` | Register custom matcher |
|
|
683
|
+
|
|
684
|
+
## License
|
|
685
|
+
|
|
686
|
+
AGPL-3.0-only
|
|
687
|
+
|
|
688
|
+
See [LICENSE](LICENSE) for details.
|