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