smartest 0.2.0.alpha2 → 0.2.0.alpha4
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/CHANGELOG.md +1 -1
- data/README.md +48 -0
- data/SMARTEST_DESIGN.md +20 -2
- data/lib/smartest/expectation_target.rb +6 -0
- data/lib/smartest/matchers.rb +484 -9
- data/lib/smartest/version.rb +1 -1
- data/smartest/smartest_test.rb +281 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 944d35bc0a1be2293c8e276d16436693ff7dd6a2d37218f9f65af3b7a47f90f5
|
|
4
|
+
data.tar.gz: edd55fb6442a92e74146eaffc76a3b881463a16b4ce3c72a877734e0f6e36f52
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5406cfcb57b56c71140c98565da884a184b8d14dcd41f8d6f65f218010d5ef7672eee6c3ad874dcc22a2d415097c9f9926f3a22c31ea3f5dafcf6dd00c2c795d
|
|
7
|
+
data.tar.gz: '0666928711e5ec22a5f9d671714597eb9e0a7e98edcf11d334bf72301c42739a8ae71104dff5784fa4728c10c186188e9f5278137ebbf7d22f6e14e745964927'
|
data/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- Support required keyword-argument fixture injection and fixture dependencies.
|
|
9
9
|
- Support per-test fixture caching and cleanup.
|
|
10
10
|
- Support suite-scoped fixtures through `suite_fixture`.
|
|
11
|
-
- Support `eq`, `include`, `be_nil`, and `
|
|
11
|
+
- Support `eq`, `include`, `start_with`, `end_with`, `be_nil`, `raise_error`, and `change` matchers.
|
|
12
12
|
- Support custom matcher modules through `use_matcher`.
|
|
13
13
|
- Generate an opt-in `PredicateMatcher` custom matcher for `be_<predicate>` calls.
|
|
14
14
|
- Add the `smartest` CLI.
|
data/README.md
CHANGED
|
@@ -178,6 +178,10 @@ Smartest uses an expectation style:
|
|
|
178
178
|
```ruby
|
|
179
179
|
expect(actual).to eq(expected)
|
|
180
180
|
expect(actual).not_to eq(expected)
|
|
181
|
+
expect { action }.to raise_error(ErrorClass)
|
|
182
|
+
expect { action }.to raise_error(/message/)
|
|
183
|
+
expect { action }.to raise_error(ErrorClass, /message/)
|
|
184
|
+
expect { action }.to change { value }
|
|
181
185
|
```
|
|
182
186
|
|
|
183
187
|
Examples:
|
|
@@ -190,6 +194,26 @@ end
|
|
|
190
194
|
test("array") do
|
|
191
195
|
expect([1, 2, 3]).to include(2)
|
|
192
196
|
end
|
|
197
|
+
|
|
198
|
+
test("URL") do
|
|
199
|
+
expect("about:blank").to start_with("about:")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
test("download") do
|
|
203
|
+
expect("screenshot.png").to end_with(".png")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
test("type") do
|
|
207
|
+
expect("smartest").to be_a(String)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
test("URL pattern") do
|
|
211
|
+
expect("https://example.test").to match(%r{\Ahttps://})
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
test("events") do
|
|
215
|
+
expect(%i[request close open]).to contain_exactly(:open, :request, :close)
|
|
216
|
+
end
|
|
193
217
|
```
|
|
194
218
|
|
|
195
219
|
Supported matchers include:
|
|
@@ -197,10 +221,34 @@ Supported matchers include:
|
|
|
197
221
|
```ruby
|
|
198
222
|
eq(expected)
|
|
199
223
|
include(expected)
|
|
224
|
+
start_with(prefix, ...)
|
|
225
|
+
end_with(suffix, ...)
|
|
226
|
+
be_a(ClassOrModule)
|
|
227
|
+
be_an(ClassOrModule)
|
|
200
228
|
be_nil
|
|
229
|
+
match(regexp)
|
|
230
|
+
contain_exactly(item, ...)
|
|
231
|
+
match_array(items)
|
|
201
232
|
raise_error(ErrorClass)
|
|
233
|
+
raise_error(/message/)
|
|
234
|
+
raise_error(ErrorClass, /message/)
|
|
235
|
+
change { value }
|
|
236
|
+
change { value }.from(before).to(after)
|
|
237
|
+
change { value }.by(delta)
|
|
202
238
|
```
|
|
203
239
|
|
|
240
|
+
`raise_error` accepts an error class, a message regexp, or both. Use an error
|
|
241
|
+
class to check the raised exception class, a regexp to check the raised
|
|
242
|
+
exception message, or both to require both conditions. No-argument and exact
|
|
243
|
+
string message forms are not supported.
|
|
244
|
+
|
|
245
|
+
`contain_exactly` and `match_array` compare collections without requiring a
|
|
246
|
+
specific order, preserve duplicate counts, and can use matcher objects such as
|
|
247
|
+
`match(/foo/)` or `eq(42)` as expected items.
|
|
248
|
+
|
|
249
|
+
`change` is only supported with `expect { ... }` block expectations and must be
|
|
250
|
+
written with a value block.
|
|
251
|
+
|
|
204
252
|
Custom matcher modules can be registered from `around_suite` or `around_test`
|
|
205
253
|
with `use_matcher`. The generated scaffold includes a `PredicateMatcher` custom
|
|
206
254
|
matcher for `be_<predicate>` calls. See [Matchers](documentation/docs/matchers.md).
|
data/SMARTEST_DESIGN.md
CHANGED
|
@@ -717,6 +717,26 @@ MVP expectation API:
|
|
|
717
717
|
```ruby
|
|
718
718
|
expect(actual).to eq(expected)
|
|
719
719
|
expect(actual).not_to eq(expected)
|
|
720
|
+
expect { action }.to change { value }
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
Current built-in matchers include:
|
|
724
|
+
|
|
725
|
+
```ruby
|
|
726
|
+
eq(expected)
|
|
727
|
+
include(expected)
|
|
728
|
+
start_with(prefix, ...)
|
|
729
|
+
end_with(suffix, ...)
|
|
730
|
+
be_a(ClassOrModule)
|
|
731
|
+
be_an(ClassOrModule)
|
|
732
|
+
be_nil
|
|
733
|
+
match(regexp)
|
|
734
|
+
contain_exactly(item, ...)
|
|
735
|
+
match_array(items)
|
|
736
|
+
raise_error(ErrorClass)
|
|
737
|
+
raise_error(/message/)
|
|
738
|
+
raise_error(ErrorClass, /message/)
|
|
739
|
+
change { value }
|
|
720
740
|
```
|
|
721
741
|
|
|
722
742
|
Internal model:
|
|
@@ -1235,8 +1255,6 @@ Possible future features:
|
|
|
1235
1255
|
- custom reporters
|
|
1236
1256
|
- JSON output
|
|
1237
1257
|
- richer matchers
|
|
1238
|
-
- block expectations
|
|
1239
|
-
- `raise_error`
|
|
1240
1258
|
- file-scoped fixtures
|
|
1241
1259
|
- parallel execution
|
|
1242
1260
|
- watch mode
|
|
@@ -13,6 +13,12 @@ module Smartest
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def not_to(matcher)
|
|
16
|
+
if matcher.respond_to?(:does_not_match?)
|
|
17
|
+
return self if matcher.does_not_match?(@actual)
|
|
18
|
+
|
|
19
|
+
raise AssertionFailed, matcher.negated_failure_message
|
|
20
|
+
end
|
|
21
|
+
|
|
16
22
|
return self unless matcher.matches?(@actual)
|
|
17
23
|
|
|
18
24
|
raise AssertionFailed, matcher.negated_failure_message
|
data/lib/smartest/matchers.rb
CHANGED
|
@@ -10,12 +10,47 @@ module Smartest
|
|
|
10
10
|
IncludeMatcher.new(expected)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def start_with(*prefixes)
|
|
14
|
+
StartWithMatcher.new(*prefixes)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def end_with(*suffixes)
|
|
18
|
+
EndWithMatcher.new(*suffixes)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def be_a(expected_class)
|
|
22
|
+
BeAKindOfMatcher.new(expected_class)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def be_an(expected_class)
|
|
26
|
+
BeAKindOfMatcher.new(expected_class)
|
|
27
|
+
end
|
|
28
|
+
|
|
13
29
|
def be_nil
|
|
14
30
|
BeNilMatcher.new
|
|
15
31
|
end
|
|
16
32
|
|
|
17
|
-
def
|
|
18
|
-
|
|
33
|
+
def match(regexp)
|
|
34
|
+
MatchMatcher.new(regexp)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def contain_exactly(*expected_items)
|
|
38
|
+
ContainExactlyMatcher.new(expected_items, matcher_name: "contain exactly")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def match_array(expected_items)
|
|
42
|
+
ContainExactlyMatcher.new(expected_items, matcher_name: "match array")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def raise_error(*expected_error)
|
|
46
|
+
RaiseErrorMatcher.new(*expected_error)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def change(*args, &block)
|
|
50
|
+
raise ArgumentError, "change does not support arguments; use change { ... }" if args.any?
|
|
51
|
+
raise ArgumentError, "change requires a block" unless block
|
|
52
|
+
|
|
53
|
+
ChangeMatcher.new(block)
|
|
19
54
|
end
|
|
20
55
|
end
|
|
21
56
|
|
|
@@ -36,6 +71,10 @@ module Smartest
|
|
|
36
71
|
def negated_failure_message
|
|
37
72
|
"expected #{@actual.inspect} not to eq #{@expected.inspect}"
|
|
38
73
|
end
|
|
74
|
+
|
|
75
|
+
def description
|
|
76
|
+
"eq #{@expected.inspect}"
|
|
77
|
+
end
|
|
39
78
|
end
|
|
40
79
|
|
|
41
80
|
class IncludeMatcher
|
|
@@ -57,6 +96,111 @@ module Smartest
|
|
|
57
96
|
def negated_failure_message
|
|
58
97
|
"expected #{@actual.inspect} not to include #{@expected.inspect}"
|
|
59
98
|
end
|
|
99
|
+
|
|
100
|
+
def description
|
|
101
|
+
"include #{@expected.inspect}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class StartWithMatcher
|
|
106
|
+
def initialize(*prefixes)
|
|
107
|
+
@prefixes = prefixes
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def matches?(actual)
|
|
111
|
+
@actual = actual
|
|
112
|
+
actual.start_with?(*@prefixes)
|
|
113
|
+
rescue NoMethodError
|
|
114
|
+
false
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def failure_message
|
|
118
|
+
"expected #{@actual.inspect} to start with #{expected_description}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def negated_failure_message
|
|
122
|
+
"expected #{@actual.inspect} not to start with #{expected_description}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def description
|
|
126
|
+
"start with #{expected_description}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def expected_description
|
|
132
|
+
return "no prefixes" if @prefixes.empty?
|
|
133
|
+
|
|
134
|
+
@prefixes.map(&:inspect).join(" or ")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class EndWithMatcher
|
|
139
|
+
def initialize(*suffixes)
|
|
140
|
+
@suffixes = suffixes
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def matches?(actual)
|
|
144
|
+
@actual = actual
|
|
145
|
+
actual.end_with?(*@suffixes)
|
|
146
|
+
rescue NoMethodError
|
|
147
|
+
false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def failure_message
|
|
151
|
+
"expected #{@actual.inspect} to end with #{expected_description}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def negated_failure_message
|
|
155
|
+
"expected #{@actual.inspect} not to end with #{expected_description}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def description
|
|
159
|
+
"end with #{expected_description}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def expected_description
|
|
165
|
+
return "no suffixes" if @suffixes.empty?
|
|
166
|
+
|
|
167
|
+
@suffixes.map(&:inspect).join(" or ")
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
class BeAKindOfMatcher
|
|
172
|
+
def initialize(expected_class)
|
|
173
|
+
@expected_class = expected_class
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def matches?(actual)
|
|
177
|
+
@actual = actual
|
|
178
|
+
actual.is_a?(@expected_class)
|
|
179
|
+
rescue TypeError
|
|
180
|
+
false
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def failure_message
|
|
184
|
+
"expected #{@actual.inspect} to be a kind of #{expected_description}, but was #{actual_class_description}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def negated_failure_message
|
|
188
|
+
"expected #{@actual.inspect} not to be a kind of #{expected_description}, but was #{actual_class_description}"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def description
|
|
192
|
+
"be a kind of #{expected_description}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
def expected_description
|
|
198
|
+
@expected_class.to_s
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def actual_class_description
|
|
202
|
+
@actual.class.to_s
|
|
203
|
+
end
|
|
60
204
|
end
|
|
61
205
|
|
|
62
206
|
class BeNilMatcher
|
|
@@ -72,11 +216,164 @@ module Smartest
|
|
|
72
216
|
def negated_failure_message
|
|
73
217
|
"expected #{@actual.inspect} not to be nil"
|
|
74
218
|
end
|
|
219
|
+
|
|
220
|
+
def description
|
|
221
|
+
"be nil"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
class MatchMatcher
|
|
226
|
+
def initialize(regexp)
|
|
227
|
+
@regexp = regexp
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def matches?(actual)
|
|
231
|
+
@actual = actual
|
|
232
|
+
@regexp.match?(actual)
|
|
233
|
+
rescue NoMethodError, TypeError
|
|
234
|
+
false
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def failure_message
|
|
238
|
+
"expected #{@actual.inspect} to match #{@regexp.inspect}"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def negated_failure_message
|
|
242
|
+
"expected #{@actual.inspect} not to match #{@regexp.inspect}"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def description
|
|
246
|
+
"match #{@regexp.inspect}"
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
class ContainExactlyMatcher
|
|
251
|
+
def initialize(expected_items, matcher_name:)
|
|
252
|
+
@expected_items = expected_items
|
|
253
|
+
@matcher_name = matcher_name
|
|
254
|
+
reset_result
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def matches?(actual)
|
|
258
|
+
@actual = actual
|
|
259
|
+
reset_result
|
|
260
|
+
return false unless actual_items?
|
|
261
|
+
|
|
262
|
+
match_items
|
|
263
|
+
@missing_items.empty? && @extra_items.empty?
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def failure_message
|
|
267
|
+
details = failure_details
|
|
268
|
+
message = "expected #{@actual.inspect} to #{@matcher_name} #{format_expected_items(@expected_items)}"
|
|
269
|
+
details.empty? ? message : "#{message}; #{details.join('; ')}"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def negated_failure_message
|
|
273
|
+
"expected #{@actual.inspect} not to #{@matcher_name} #{format_expected_items(@expected_items)}"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def description
|
|
277
|
+
"#{@matcher_name} #{format_expected_items(@expected_items)}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
private
|
|
281
|
+
|
|
282
|
+
def reset_result
|
|
283
|
+
@actual_items = nil
|
|
284
|
+
@missing_items = @expected_items.dup
|
|
285
|
+
@extra_items = []
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def actual_items?
|
|
289
|
+
return false unless @actual.respond_to?(:to_a)
|
|
290
|
+
|
|
291
|
+
@actual_items = @actual.to_a
|
|
292
|
+
true
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def match_items
|
|
296
|
+
adjacency = build_adjacency
|
|
297
|
+
actual_matches = Array.new(@actual_items.length)
|
|
298
|
+
expected_order(adjacency).each do |expected_index|
|
|
299
|
+
assign_expected_item(expected_index, adjacency, actual_matches, [])
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
matched_expected_indexes = actual_matches.compact
|
|
303
|
+
@missing_items = []
|
|
304
|
+
@expected_items.each_with_index do |item, index|
|
|
305
|
+
@missing_items << item unless matched_expected_indexes.include?(index)
|
|
306
|
+
end
|
|
307
|
+
@extra_items = []
|
|
308
|
+
@actual_items.each_with_index do |item, index|
|
|
309
|
+
@extra_items << item if actual_matches[index].nil?
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def build_adjacency
|
|
314
|
+
@expected_items.map do |expected_item|
|
|
315
|
+
@actual_items.each_index.select do |actual_index|
|
|
316
|
+
expected_item_matches_actual_item?(expected_item, @actual_items[actual_index])
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def expected_order(adjacency)
|
|
322
|
+
(0...@expected_items.length).sort_by { |index| [adjacency[index].length, index] }
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def assign_expected_item(expected_index, adjacency, actual_matches, seen_actual_indexes)
|
|
326
|
+
adjacency[expected_index].each do |actual_index|
|
|
327
|
+
next if seen_actual_indexes.include?(actual_index)
|
|
328
|
+
|
|
329
|
+
seen_actual_indexes << actual_index
|
|
330
|
+
if actual_matches[actual_index].nil? ||
|
|
331
|
+
assign_expected_item(actual_matches[actual_index], adjacency, actual_matches, seen_actual_indexes)
|
|
332
|
+
actual_matches[actual_index] = expected_index
|
|
333
|
+
return true
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
false
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def expected_item_matches_actual_item?(expected_item, actual_item)
|
|
341
|
+
if expected_item.respond_to?(:matches?)
|
|
342
|
+
expected_item.matches?(actual_item)
|
|
343
|
+
else
|
|
344
|
+
actual_item == expected_item
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def failure_details
|
|
349
|
+
return ["actual did not provide items"] unless @actual_items
|
|
350
|
+
|
|
351
|
+
details = []
|
|
352
|
+
details << "missing: #{format_expected_items(@missing_items)}" unless @missing_items.empty?
|
|
353
|
+
details << "extra: #{format_actual_items(@extra_items)}" unless @extra_items.empty?
|
|
354
|
+
details
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def format_expected_items(items)
|
|
358
|
+
"[#{items.map { |item| format_expected_item(item) }.join(', ')}]"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def format_actual_items(items)
|
|
362
|
+
"[#{items.map(&:inspect).join(', ')}]"
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def format_expected_item(item)
|
|
366
|
+
return item.description if item.respond_to?(:description)
|
|
367
|
+
|
|
368
|
+
item.inspect
|
|
369
|
+
end
|
|
75
370
|
end
|
|
76
371
|
|
|
77
372
|
class RaiseErrorMatcher
|
|
78
|
-
def initialize(expected_error)
|
|
79
|
-
@
|
|
373
|
+
def initialize(*expected_error)
|
|
374
|
+
@expected_type = expected_type_for(expected_error)
|
|
375
|
+
@expected_error_class = expected_error.find { |item| error_class?(item) }
|
|
376
|
+
@expected_message_regexp = expected_error.find { |item| item.is_a?(Regexp) }
|
|
80
377
|
@actual_error = nil
|
|
81
378
|
@callable = true
|
|
82
379
|
end
|
|
@@ -92,18 +389,196 @@ module Smartest
|
|
|
92
389
|
raise if Smartest.fatal_exception?(error)
|
|
93
390
|
|
|
94
391
|
@actual_error = error
|
|
95
|
-
|
|
392
|
+
expected_error_matches?(error)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def failure_message
|
|
396
|
+
return "expected a block to raise #{expected_description}" unless @callable
|
|
397
|
+
return "expected block to raise #{expected_description}, but nothing was raised" unless @actual_error
|
|
398
|
+
|
|
399
|
+
"expected block to raise #{expected_description}, but raised #{actual_error_description}"
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def negated_failure_message
|
|
403
|
+
"expected block not to raise #{expected_description}, but raised #{actual_error_description}"
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
private
|
|
407
|
+
|
|
408
|
+
def expected_type_for(expected_error)
|
|
409
|
+
return :class if expected_error.length == 1 && error_class?(expected_error.first)
|
|
410
|
+
return :message_regexp if expected_error.length == 1 && expected_error.first.is_a?(Regexp)
|
|
411
|
+
return :class_and_message_regexp if expected_error.length == 2 &&
|
|
412
|
+
error_class?(expected_error[0]) &&
|
|
413
|
+
expected_error[1].is_a?(Regexp)
|
|
414
|
+
|
|
415
|
+
raise ArgumentError, "raise_error supports an error class, message regexp, or error class and message regexp"
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def error_class?(value)
|
|
419
|
+
value.is_a?(Class) && value <= Exception
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def expected_error_matches?(error)
|
|
423
|
+
case @expected_type
|
|
424
|
+
when :class
|
|
425
|
+
error.is_a?(@expected_error_class)
|
|
426
|
+
when :message_regexp
|
|
427
|
+
@expected_message_regexp.match?(error.message)
|
|
428
|
+
when :class_and_message_regexp
|
|
429
|
+
error.is_a?(@expected_error_class) && @expected_message_regexp.match?(error.message)
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def expected_description
|
|
434
|
+
case @expected_type
|
|
435
|
+
when :class
|
|
436
|
+
@expected_error_class.to_s
|
|
437
|
+
when :message_regexp
|
|
438
|
+
"error with message matching #{@expected_message_regexp.inspect}"
|
|
439
|
+
when :class_and_message_regexp
|
|
440
|
+
"#{@expected_error_class} with message matching #{@expected_message_regexp.inspect}"
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def actual_error_description
|
|
445
|
+
"#{@actual_error.class}: #{@actual_error.message}"
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
class ChangeMatcher
|
|
450
|
+
UNSET = Object.new
|
|
451
|
+
|
|
452
|
+
def initialize(value_block)
|
|
453
|
+
@value_block = value_block
|
|
454
|
+
@expected_from = UNSET
|
|
455
|
+
@expected_to = UNSET
|
|
456
|
+
@expected_delta = UNSET
|
|
457
|
+
reset_result
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def from(expected)
|
|
461
|
+
@expected_from = expected
|
|
462
|
+
self
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def to(expected)
|
|
466
|
+
@expected_to = expected
|
|
467
|
+
self
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def by(expected_delta)
|
|
471
|
+
@expected_delta = expected_delta
|
|
472
|
+
self
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def matches?(actual)
|
|
476
|
+
run_change(actual)
|
|
477
|
+
return false unless @callable
|
|
478
|
+
|
|
479
|
+
positive_failures.empty?
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def does_not_match?(actual)
|
|
483
|
+
run_change(actual)
|
|
484
|
+
return false unless @callable
|
|
485
|
+
|
|
486
|
+
negated_failures.empty?
|
|
96
487
|
end
|
|
97
488
|
|
|
98
489
|
def failure_message
|
|
99
|
-
return "expected a block to
|
|
100
|
-
return "expected block to raise #{@expected_error}, but nothing was raised" unless @actual_error
|
|
490
|
+
return "expected a block to change value" unless @callable
|
|
101
491
|
|
|
102
|
-
"expected
|
|
492
|
+
"expected value to #{expected_description}, but #{observed_description}#{failed_modifier_description}"
|
|
103
493
|
end
|
|
104
494
|
|
|
105
495
|
def negated_failure_message
|
|
106
|
-
"expected block not to
|
|
496
|
+
return "expected a block not to change value" unless @callable
|
|
497
|
+
|
|
498
|
+
"expected value not to change, but #{observed_description}"
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
private
|
|
502
|
+
|
|
503
|
+
def reset_result
|
|
504
|
+
@callable = true
|
|
505
|
+
@before_value = nil
|
|
506
|
+
@after_value = nil
|
|
507
|
+
@actual_delta = UNSET
|
|
508
|
+
@failed_modifiers = []
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def run_change(actual)
|
|
512
|
+
reset_result
|
|
513
|
+
@callable = actual.respond_to?(:call)
|
|
514
|
+
return unless @callable
|
|
515
|
+
|
|
516
|
+
@before_value = @value_block.call
|
|
517
|
+
actual.call
|
|
518
|
+
@after_value = @value_block.call
|
|
519
|
+
calculate_delta if delta_expected?
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def positive_failures
|
|
523
|
+
@failed_modifiers = []
|
|
524
|
+
@failed_modifiers << "change" if !delta_expected? && @before_value == @after_value
|
|
525
|
+
@failed_modifiers << "from(#{@expected_from.inspect})" if from_expected? && @before_value != @expected_from
|
|
526
|
+
@failed_modifiers << "to(#{@expected_to.inspect})" if to_expected? && @after_value != @expected_to
|
|
527
|
+
@failed_modifiers << "by(#{@expected_delta.inspect})" if delta_expected? && @actual_delta != @expected_delta
|
|
528
|
+
@failed_modifiers
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def negated_failures
|
|
532
|
+
@failed_modifiers = []
|
|
533
|
+
@failed_modifiers << "change" unless @before_value == @after_value
|
|
534
|
+
@failed_modifiers
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def expected_description
|
|
538
|
+
parts = ["change"]
|
|
539
|
+
parts << "from #{@expected_from.inspect}" if from_expected?
|
|
540
|
+
parts << "to #{@expected_to.inspect}" if to_expected?
|
|
541
|
+
parts << "by #{@expected_delta.inspect}" if delta_expected?
|
|
542
|
+
parts.join(" ")
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def observed_description
|
|
546
|
+
if delta_expected?
|
|
547
|
+
delta_description = if @actual_delta.equal?(UNSET)
|
|
548
|
+
"could not calculate a numeric difference"
|
|
549
|
+
else
|
|
550
|
+
"changed by #{@actual_delta.inspect}"
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
"#{delta_description} from #{@before_value.inspect} before to #{@after_value.inspect} after"
|
|
554
|
+
else
|
|
555
|
+
"was #{@before_value.inspect} before and #{@after_value.inspect} after"
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def failed_modifier_description
|
|
560
|
+
failures = positive_failures
|
|
561
|
+
return "" if failures.empty?
|
|
562
|
+
|
|
563
|
+
"; failed modifiers: #{failures.join(', ')}"
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def calculate_delta
|
|
567
|
+
@actual_delta = @after_value - @before_value
|
|
568
|
+
rescue NoMethodError, TypeError
|
|
569
|
+
@actual_delta = UNSET
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def from_expected?
|
|
573
|
+
!@expected_from.equal?(UNSET)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def to_expected?
|
|
577
|
+
!@expected_to.equal?(UNSET)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def delta_expected?
|
|
581
|
+
!@expected_delta.equal?(UNSET)
|
|
107
582
|
end
|
|
108
583
|
end
|
|
109
584
|
end
|
data/lib/smartest/version.rb
CHANGED
data/smartest/smartest_test.rb
CHANGED
|
@@ -48,6 +48,15 @@ class SelfTestRegisteredFixture < Smartest::Fixture
|
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
class SelfTestBaseType; end
|
|
52
|
+
class SelfTestChildType < SelfTestBaseType; end
|
|
53
|
+
|
|
54
|
+
module SelfTestMarkerType; end
|
|
55
|
+
|
|
56
|
+
class SelfTestMarkedType
|
|
57
|
+
include SelfTestMarkerType
|
|
58
|
+
end
|
|
59
|
+
|
|
51
60
|
around_suite do |suite|
|
|
52
61
|
use_fixture SelfTestRegisteredFixture
|
|
53
62
|
suite.run
|
|
@@ -172,9 +181,34 @@ test("supports basic matchers") do
|
|
|
172
181
|
"matchers",
|
|
173
182
|
proc do
|
|
174
183
|
expect([1, 2, 3]).to include(2)
|
|
184
|
+
expect("about:blank").to start_with("about:")
|
|
185
|
+
expect("https://cdn-b.test/assets/app.js").to start_with(
|
|
186
|
+
"https://cdn-a.test",
|
|
187
|
+
"https://cdn-b.test"
|
|
188
|
+
)
|
|
189
|
+
expect("screenshot.png").to end_with(".jpg", ".png")
|
|
190
|
+
expect("https://example.test").not_to start_with("about:")
|
|
191
|
+
expect("report.txt").not_to end_with(".png")
|
|
192
|
+
expect(Object.new).not_to start_with("prefix")
|
|
193
|
+
expect(SelfTestChildType.new).to be_a(SelfTestBaseType)
|
|
194
|
+
expect(SelfTestMarkedType.new).to be_an(SelfTestMarkerType)
|
|
175
195
|
expect(nil).to be_nil
|
|
176
196
|
expect("value").not_to be_nil
|
|
197
|
+
expect("https://example.test").to match(%r{\Ahttps://})
|
|
198
|
+
expect("about:blank").not_to match(%r{\Ahttps://})
|
|
199
|
+
expect(%w[request close request]).to contain_exactly("request", "request", "close")
|
|
200
|
+
expect(%i[request close open]).to match_array(%i[open request close])
|
|
201
|
+
expect(["foo", 42]).to contain_exactly(match(/foo/), eq(42))
|
|
202
|
+
expect(["ab", "ac"]).to contain_exactly(match(/a/), "ab")
|
|
203
|
+
expect([nil, false]).to contain_exactly(false, nil)
|
|
204
|
+
expect([:request]).not_to contain_exactly(:request, :request)
|
|
177
205
|
expect { raise ArgumentError, "bad" }.to raise_error(ArgumentError)
|
|
206
|
+
expect { raise RuntimeError, "request timed out" }.to raise_error(/timed out/)
|
|
207
|
+
expect { raise ArgumentError, "bad input" }.to raise_error(ArgumentError, /bad/)
|
|
208
|
+
expect { raise ArgumentError, "bad" }.not_to raise_error(RuntimeError)
|
|
209
|
+
expect { raise ArgumentError, "bad" }.not_to raise_error(/timed out/)
|
|
210
|
+
expect { raise ArgumentError, "bad" }.not_to raise_error(ArgumentError, /timed out/)
|
|
211
|
+
expect { :ok }.not_to raise_error(/timed out/)
|
|
178
212
|
end
|
|
179
213
|
)
|
|
180
214
|
)
|
|
@@ -184,6 +218,253 @@ test("supports basic matchers") do
|
|
|
184
218
|
expect(status).to eq(0)
|
|
185
219
|
end
|
|
186
220
|
|
|
221
|
+
test("reports be_a and match matcher failures") do
|
|
222
|
+
suite = Smartest::Suite.new
|
|
223
|
+
suite.tests.add(
|
|
224
|
+
SmartestSelfTest.test_case(
|
|
225
|
+
"bad type",
|
|
226
|
+
proc { expect(nil).to be_a(String) }
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
suite.tests.add(
|
|
230
|
+
SmartestSelfTest.test_case(
|
|
231
|
+
"bad negated type",
|
|
232
|
+
proc { expect(SelfTestChildType.new).not_to be_an(SelfTestBaseType) }
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
suite.tests.add(
|
|
236
|
+
SmartestSelfTest.test_case(
|
|
237
|
+
"bad regex",
|
|
238
|
+
proc { expect("about:blank").to match(%r{\Ahttps://}) }
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
suite.tests.add(
|
|
242
|
+
SmartestSelfTest.test_case(
|
|
243
|
+
"bad negated regex",
|
|
244
|
+
proc { expect("https://example.test").not_to match(%r{\Ahttps://}) }
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
status, output = SmartestSelfTest.run_suite(suite)
|
|
249
|
+
|
|
250
|
+
expect(status).to eq(1)
|
|
251
|
+
expect(output).to include("expected nil to be a kind of String, but was NilClass")
|
|
252
|
+
expect(output).to include("not to be a kind of SelfTestBaseType, but was SelfTestChildType")
|
|
253
|
+
expect(output).to include('expected "about:blank" to match /\\Ahttps:\\/\\//')
|
|
254
|
+
expect(output).to include('expected "https://example.test" not to match /\\Ahttps:\\/\\//')
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
test("reports contain_exactly and match_array matcher failures") do
|
|
258
|
+
suite = Smartest::Suite.new
|
|
259
|
+
suite.tests.add(
|
|
260
|
+
SmartestSelfTest.test_case(
|
|
261
|
+
"bad collection",
|
|
262
|
+
proc { expect(["foo", "baz", 2]).to contain_exactly(match(/foo/), eq(42), "bar") }
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
suite.tests.add(
|
|
266
|
+
SmartestSelfTest.test_case(
|
|
267
|
+
"bad duplicate count",
|
|
268
|
+
proc { expect([:request]).to match_array(%i[request request]) }
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
suite.tests.add(
|
|
272
|
+
SmartestSelfTest.test_case(
|
|
273
|
+
"bad negated collection",
|
|
274
|
+
proc { expect(%w[b a]).not_to contain_exactly("a", "b") }
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
status, output = SmartestSelfTest.run_suite(suite)
|
|
279
|
+
|
|
280
|
+
expect(status).to eq(1)
|
|
281
|
+
expect(output).to include('expected ["foo", "baz", 2] to contain exactly [match /foo/, eq 42, "bar"]')
|
|
282
|
+
expect(output).to include('missing: [eq 42, "bar"]')
|
|
283
|
+
expect(output).to include('extra: ["baz", 2]')
|
|
284
|
+
expect(output).to include("expected [:request] to match array [:request, :request]")
|
|
285
|
+
expect(output).to include("missing: [:request]")
|
|
286
|
+
expect(output).to include('expected ["b", "a"] not to contain exactly ["a", "b"]')
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
test("reports raise_error matcher failures") do
|
|
290
|
+
suite = Smartest::Suite.new
|
|
291
|
+
suite.tests.add(
|
|
292
|
+
SmartestSelfTest.test_case(
|
|
293
|
+
"nothing raised",
|
|
294
|
+
proc { expect { :ok }.to raise_error(/timeout/) }
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
suite.tests.add(
|
|
298
|
+
SmartestSelfTest.test_case(
|
|
299
|
+
"bad message",
|
|
300
|
+
proc { expect { raise RuntimeError, "permission denied" }.to raise_error(/timeout/) }
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
suite.tests.add(
|
|
304
|
+
SmartestSelfTest.test_case(
|
|
305
|
+
"bad negated message",
|
|
306
|
+
proc { expect { raise RuntimeError, "timeout after 1s" }.not_to raise_error(/timeout/) }
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
suite.tests.add(
|
|
310
|
+
SmartestSelfTest.test_case(
|
|
311
|
+
"bad class and message class",
|
|
312
|
+
proc { expect { raise RuntimeError, "timeout after 1s" }.to raise_error(ArgumentError, /timeout/) }
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
suite.tests.add(
|
|
316
|
+
SmartestSelfTest.test_case(
|
|
317
|
+
"bad class and message message",
|
|
318
|
+
proc { expect { raise ArgumentError, "permission denied" }.to raise_error(ArgumentError, /timeout/) }
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
suite.tests.add(
|
|
322
|
+
SmartestSelfTest.test_case(
|
|
323
|
+
"bad negated class and message",
|
|
324
|
+
proc { expect { raise ArgumentError, "timeout after 1s" }.not_to raise_error(ArgumentError, /timeout/) }
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
status, output = SmartestSelfTest.run_suite(suite)
|
|
329
|
+
|
|
330
|
+
expect(status).to eq(1)
|
|
331
|
+
expect(output).to include("expected block to raise error with message matching /timeout/, but nothing was raised")
|
|
332
|
+
expect(output).to include(
|
|
333
|
+
"expected block to raise error with message matching /timeout/, but raised RuntimeError: permission denied"
|
|
334
|
+
)
|
|
335
|
+
expect(output).to include(
|
|
336
|
+
"expected block not to raise error with message matching /timeout/, but raised RuntimeError: timeout after 1s"
|
|
337
|
+
)
|
|
338
|
+
expect(output).to include(
|
|
339
|
+
"expected block to raise ArgumentError with message matching /timeout/, but raised RuntimeError: timeout after 1s"
|
|
340
|
+
)
|
|
341
|
+
expect(output).to include(
|
|
342
|
+
"expected block to raise ArgumentError with message matching /timeout/, but raised ArgumentError: permission denied"
|
|
343
|
+
)
|
|
344
|
+
expect(output).to include(
|
|
345
|
+
"expected block not to raise ArgumentError with message matching /timeout/, but raised ArgumentError: timeout after 1s"
|
|
346
|
+
)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
test("rejects unsupported raise_error argument forms") do
|
|
350
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
351
|
+
raise_error
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
expect(error.message).to eq("raise_error supports an error class, message regexp, or error class and message regexp")
|
|
355
|
+
|
|
356
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
357
|
+
raise_error("timeout")
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
expect(error.message).to eq("raise_error supports an error class, message regexp, or error class and message regexp")
|
|
361
|
+
|
|
362
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
363
|
+
raise_error(String)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
expect(error.message).to eq("raise_error supports an error class, message regexp, or error class and message regexp")
|
|
367
|
+
|
|
368
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
369
|
+
raise_error(RuntimeError, "timeout")
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
expect(error.message).to eq("raise_error supports an error class, message regexp, or error class and message regexp")
|
|
373
|
+
|
|
374
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
375
|
+
raise_error(/timeout/, RuntimeError)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
expect(error.message).to eq("raise_error supports an error class, message regexp, or error class and message regexp")
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
test("reports start_with and end_with matcher failures") do
|
|
382
|
+
suite = Smartest::Suite.new
|
|
383
|
+
suite.tests.add(
|
|
384
|
+
SmartestSelfTest.test_case(
|
|
385
|
+
"bad prefix",
|
|
386
|
+
proc { expect("https://example.test/path").to start_with("about:") }
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
suite.tests.add(
|
|
390
|
+
SmartestSelfTest.test_case(
|
|
391
|
+
"bad suffix",
|
|
392
|
+
proc { expect("asset.gif").to end_with(".jpg", ".png") }
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
suite.tests.add(
|
|
396
|
+
SmartestSelfTest.test_case(
|
|
397
|
+
"bad negated suffix",
|
|
398
|
+
proc { expect("screenshot.png").not_to end_with(".png") }
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
status, output = SmartestSelfTest.run_suite(suite)
|
|
403
|
+
|
|
404
|
+
expect(status).to eq(1)
|
|
405
|
+
expect(output).to include('expected "https://example.test/path" to start with "about:"')
|
|
406
|
+
expect(output).to include('expected "asset.gif" to end with ".jpg" or ".png"')
|
|
407
|
+
expect(output).to include('expected "screenshot.png" not to end with ".png"')
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
test("supports change matcher for block expectations") do
|
|
411
|
+
value = 0
|
|
412
|
+
action_calls = 0
|
|
413
|
+
|
|
414
|
+
expect {
|
|
415
|
+
action_calls += 1
|
|
416
|
+
value += 2
|
|
417
|
+
}.to change { value }.from(0).to(2).by(2)
|
|
418
|
+
|
|
419
|
+
expect(action_calls).to eq(1)
|
|
420
|
+
expect { value }.not_to change { value }
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
test("reports change matcher failures with before and after values") do
|
|
424
|
+
value = 0
|
|
425
|
+
|
|
426
|
+
error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
|
|
427
|
+
expect { value += 1 }.to change { value }.from(0).to(2).by(2)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
expect(error.message).to include("0 before")
|
|
431
|
+
expect(error.message).to include("1 after")
|
|
432
|
+
expect(error.message).to include("to(2)")
|
|
433
|
+
expect(error.message).to include("by(2)")
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
test("fails negated change matcher when the value changes") do
|
|
437
|
+
value = 0
|
|
438
|
+
|
|
439
|
+
error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
|
|
440
|
+
expect { value += 1 }.not_to change { value }
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
expect(error.message).to include("expected value not to change")
|
|
444
|
+
expect(error.message).to include("0 before")
|
|
445
|
+
expect(error.message).to include("1 after")
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
test("requires change matcher value and action blocks") do
|
|
449
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
450
|
+
change
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
expect(error.message).to eq("change requires a block")
|
|
454
|
+
|
|
455
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
456
|
+
change(:value) { :other }
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
expect(error.message).to include("change does not support arguments")
|
|
460
|
+
|
|
461
|
+
error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
|
|
462
|
+
expect(:value).to change { :other }
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
expect(error.message).to include("expected a block to change value")
|
|
466
|
+
end
|
|
467
|
+
|
|
187
468
|
test("registers matcher modules for suite execution contexts") do
|
|
188
469
|
status_matcher = Class.new do
|
|
189
470
|
def initialize(expected)
|