smartest 0.2.0.alpha2 → 0.2.0.alpha3
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 +18 -0
- data/SMARTEST_DESIGN.md +13 -2
- data/lib/smartest/expectation_target.rb +6 -0
- data/lib/smartest/matchers.rb +209 -0
- data/lib/smartest/version.rb +1 -1
- data/smartest/smartest_test.rb +96 -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: 17a325d82883047881ac13538f95b3af038e361668869860034acc530ce2e398
|
|
4
|
+
data.tar.gz: 19c6a1a345e7e0fd02a12ae007a48f38c1be264debd3b23b0b6085d687a867bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b4aeb31472fca3b0911dfb29d4034405f493ac37e3019888a67f1c13c0b1f4aac99dc33b87c7e79a8d363b7bf12f7682b97bea396ebc56b558b592a7658eee9
|
|
7
|
+
data.tar.gz: 4e1c0cc3b739b62af8c4eeef394662f8115e35f890cdde87ce32117eca26c4064b627a08a2ca204c856276d2a603de8c4c55e2f9c71d2f68c44fd864e13c2ecc
|
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,8 @@ 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 change { value }
|
|
181
183
|
```
|
|
182
184
|
|
|
183
185
|
Examples:
|
|
@@ -190,6 +192,14 @@ end
|
|
|
190
192
|
test("array") do
|
|
191
193
|
expect([1, 2, 3]).to include(2)
|
|
192
194
|
end
|
|
195
|
+
|
|
196
|
+
test("URL") do
|
|
197
|
+
expect("about:blank").to start_with("about:")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
test("download") do
|
|
201
|
+
expect("screenshot.png").to end_with(".png")
|
|
202
|
+
end
|
|
193
203
|
```
|
|
194
204
|
|
|
195
205
|
Supported matchers include:
|
|
@@ -197,10 +207,18 @@ Supported matchers include:
|
|
|
197
207
|
```ruby
|
|
198
208
|
eq(expected)
|
|
199
209
|
include(expected)
|
|
210
|
+
start_with(prefix, ...)
|
|
211
|
+
end_with(suffix, ...)
|
|
200
212
|
be_nil
|
|
201
213
|
raise_error(ErrorClass)
|
|
214
|
+
change { value }
|
|
215
|
+
change { value }.from(before).to(after)
|
|
216
|
+
change { value }.by(delta)
|
|
202
217
|
```
|
|
203
218
|
|
|
219
|
+
`change` is only supported with `expect { ... }` block expectations and must be
|
|
220
|
+
written with a value block.
|
|
221
|
+
|
|
204
222
|
Custom matcher modules can be registered from `around_suite` or `around_test`
|
|
205
223
|
with `use_matcher`. The generated scaffold includes a `PredicateMatcher` custom
|
|
206
224
|
matcher for `be_<predicate>` calls. See [Matchers](documentation/docs/matchers.md).
|
data/SMARTEST_DESIGN.md
CHANGED
|
@@ -717,6 +717,19 @@ 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_nil
|
|
731
|
+
raise_error(ErrorClass)
|
|
732
|
+
change { value }
|
|
720
733
|
```
|
|
721
734
|
|
|
722
735
|
Internal model:
|
|
@@ -1235,8 +1248,6 @@ Possible future features:
|
|
|
1235
1248
|
- custom reporters
|
|
1236
1249
|
- JSON output
|
|
1237
1250
|
- richer matchers
|
|
1238
|
-
- block expectations
|
|
1239
|
-
- `raise_error`
|
|
1240
1251
|
- file-scoped fixtures
|
|
1241
1252
|
- parallel execution
|
|
1242
1253
|
- 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,6 +10,14 @@ 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
|
+
|
|
13
21
|
def be_nil
|
|
14
22
|
BeNilMatcher.new
|
|
15
23
|
end
|
|
@@ -17,6 +25,13 @@ module Smartest
|
|
|
17
25
|
def raise_error(expected_error = StandardError)
|
|
18
26
|
RaiseErrorMatcher.new(expected_error)
|
|
19
27
|
end
|
|
28
|
+
|
|
29
|
+
def change(*args, &block)
|
|
30
|
+
raise ArgumentError, "change does not support arguments; use change { ... }" if args.any?
|
|
31
|
+
raise ArgumentError, "change requires a block" unless block
|
|
32
|
+
|
|
33
|
+
ChangeMatcher.new(block)
|
|
34
|
+
end
|
|
20
35
|
end
|
|
21
36
|
|
|
22
37
|
class EqMatcher
|
|
@@ -59,6 +74,64 @@ module Smartest
|
|
|
59
74
|
end
|
|
60
75
|
end
|
|
61
76
|
|
|
77
|
+
class StartWithMatcher
|
|
78
|
+
def initialize(*prefixes)
|
|
79
|
+
@prefixes = prefixes
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def matches?(actual)
|
|
83
|
+
@actual = actual
|
|
84
|
+
actual.start_with?(*@prefixes)
|
|
85
|
+
rescue NoMethodError
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def failure_message
|
|
90
|
+
"expected #{@actual.inspect} to start with #{expected_description}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def negated_failure_message
|
|
94
|
+
"expected #{@actual.inspect} not to start with #{expected_description}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def expected_description
|
|
100
|
+
return "no prefixes" if @prefixes.empty?
|
|
101
|
+
|
|
102
|
+
@prefixes.map(&:inspect).join(" or ")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class EndWithMatcher
|
|
107
|
+
def initialize(*suffixes)
|
|
108
|
+
@suffixes = suffixes
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def matches?(actual)
|
|
112
|
+
@actual = actual
|
|
113
|
+
actual.end_with?(*@suffixes)
|
|
114
|
+
rescue NoMethodError
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def failure_message
|
|
119
|
+
"expected #{@actual.inspect} to end with #{expected_description}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def negated_failure_message
|
|
123
|
+
"expected #{@actual.inspect} not to end with #{expected_description}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def expected_description
|
|
129
|
+
return "no suffixes" if @suffixes.empty?
|
|
130
|
+
|
|
131
|
+
@suffixes.map(&:inspect).join(" or ")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
62
135
|
class BeNilMatcher
|
|
63
136
|
def matches?(actual)
|
|
64
137
|
@actual = actual
|
|
@@ -106,4 +179,140 @@ module Smartest
|
|
|
106
179
|
"expected block not to raise #{@expected_error}, but raised #{@actual_error.class}: #{@actual_error.message}"
|
|
107
180
|
end
|
|
108
181
|
end
|
|
182
|
+
|
|
183
|
+
class ChangeMatcher
|
|
184
|
+
UNSET = Object.new
|
|
185
|
+
|
|
186
|
+
def initialize(value_block)
|
|
187
|
+
@value_block = value_block
|
|
188
|
+
@expected_from = UNSET
|
|
189
|
+
@expected_to = UNSET
|
|
190
|
+
@expected_delta = UNSET
|
|
191
|
+
reset_result
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def from(expected)
|
|
195
|
+
@expected_from = expected
|
|
196
|
+
self
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def to(expected)
|
|
200
|
+
@expected_to = expected
|
|
201
|
+
self
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def by(expected_delta)
|
|
205
|
+
@expected_delta = expected_delta
|
|
206
|
+
self
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def matches?(actual)
|
|
210
|
+
run_change(actual)
|
|
211
|
+
return false unless @callable
|
|
212
|
+
|
|
213
|
+
positive_failures.empty?
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def does_not_match?(actual)
|
|
217
|
+
run_change(actual)
|
|
218
|
+
return false unless @callable
|
|
219
|
+
|
|
220
|
+
negated_failures.empty?
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def failure_message
|
|
224
|
+
return "expected a block to change value" unless @callable
|
|
225
|
+
|
|
226
|
+
"expected value to #{expected_description}, but #{observed_description}#{failed_modifier_description}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def negated_failure_message
|
|
230
|
+
return "expected a block not to change value" unless @callable
|
|
231
|
+
|
|
232
|
+
"expected value not to change, but #{observed_description}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private
|
|
236
|
+
|
|
237
|
+
def reset_result
|
|
238
|
+
@callable = true
|
|
239
|
+
@before_value = nil
|
|
240
|
+
@after_value = nil
|
|
241
|
+
@actual_delta = UNSET
|
|
242
|
+
@failed_modifiers = []
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def run_change(actual)
|
|
246
|
+
reset_result
|
|
247
|
+
@callable = actual.respond_to?(:call)
|
|
248
|
+
return unless @callable
|
|
249
|
+
|
|
250
|
+
@before_value = @value_block.call
|
|
251
|
+
actual.call
|
|
252
|
+
@after_value = @value_block.call
|
|
253
|
+
calculate_delta if delta_expected?
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def positive_failures
|
|
257
|
+
@failed_modifiers = []
|
|
258
|
+
@failed_modifiers << "change" if !delta_expected? && @before_value == @after_value
|
|
259
|
+
@failed_modifiers << "from(#{@expected_from.inspect})" if from_expected? && @before_value != @expected_from
|
|
260
|
+
@failed_modifiers << "to(#{@expected_to.inspect})" if to_expected? && @after_value != @expected_to
|
|
261
|
+
@failed_modifiers << "by(#{@expected_delta.inspect})" if delta_expected? && @actual_delta != @expected_delta
|
|
262
|
+
@failed_modifiers
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def negated_failures
|
|
266
|
+
@failed_modifiers = []
|
|
267
|
+
@failed_modifiers << "change" unless @before_value == @after_value
|
|
268
|
+
@failed_modifiers
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def expected_description
|
|
272
|
+
parts = ["change"]
|
|
273
|
+
parts << "from #{@expected_from.inspect}" if from_expected?
|
|
274
|
+
parts << "to #{@expected_to.inspect}" if to_expected?
|
|
275
|
+
parts << "by #{@expected_delta.inspect}" if delta_expected?
|
|
276
|
+
parts.join(" ")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def observed_description
|
|
280
|
+
if delta_expected?
|
|
281
|
+
delta_description = if @actual_delta.equal?(UNSET)
|
|
282
|
+
"could not calculate a numeric difference"
|
|
283
|
+
else
|
|
284
|
+
"changed by #{@actual_delta.inspect}"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
"#{delta_description} from #{@before_value.inspect} before to #{@after_value.inspect} after"
|
|
288
|
+
else
|
|
289
|
+
"was #{@before_value.inspect} before and #{@after_value.inspect} after"
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def failed_modifier_description
|
|
294
|
+
failures = positive_failures
|
|
295
|
+
return "" if failures.empty?
|
|
296
|
+
|
|
297
|
+
"; failed modifiers: #{failures.join(', ')}"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def calculate_delta
|
|
301
|
+
@actual_delta = @after_value - @before_value
|
|
302
|
+
rescue NoMethodError, TypeError
|
|
303
|
+
@actual_delta = UNSET
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def from_expected?
|
|
307
|
+
!@expected_from.equal?(UNSET)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def to_expected?
|
|
311
|
+
!@expected_to.equal?(UNSET)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def delta_expected?
|
|
315
|
+
!@expected_delta.equal?(UNSET)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
109
318
|
end
|
data/lib/smartest/version.rb
CHANGED
data/smartest/smartest_test.rb
CHANGED
|
@@ -172,6 +172,15 @@ test("supports basic matchers") do
|
|
|
172
172
|
"matchers",
|
|
173
173
|
proc do
|
|
174
174
|
expect([1, 2, 3]).to include(2)
|
|
175
|
+
expect("about:blank").to start_with("about:")
|
|
176
|
+
expect("https://cdn-b.test/assets/app.js").to start_with(
|
|
177
|
+
"https://cdn-a.test",
|
|
178
|
+
"https://cdn-b.test"
|
|
179
|
+
)
|
|
180
|
+
expect("screenshot.png").to end_with(".jpg", ".png")
|
|
181
|
+
expect("https://example.test").not_to start_with("about:")
|
|
182
|
+
expect("report.txt").not_to end_with(".png")
|
|
183
|
+
expect(Object.new).not_to start_with("prefix")
|
|
175
184
|
expect(nil).to be_nil
|
|
176
185
|
expect("value").not_to be_nil
|
|
177
186
|
expect { raise ArgumentError, "bad" }.to raise_error(ArgumentError)
|
|
@@ -184,6 +193,93 @@ test("supports basic matchers") do
|
|
|
184
193
|
expect(status).to eq(0)
|
|
185
194
|
end
|
|
186
195
|
|
|
196
|
+
test("reports start_with and end_with matcher failures") do
|
|
197
|
+
suite = Smartest::Suite.new
|
|
198
|
+
suite.tests.add(
|
|
199
|
+
SmartestSelfTest.test_case(
|
|
200
|
+
"bad prefix",
|
|
201
|
+
proc { expect("https://example.test/path").to start_with("about:") }
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
suite.tests.add(
|
|
205
|
+
SmartestSelfTest.test_case(
|
|
206
|
+
"bad suffix",
|
|
207
|
+
proc { expect("asset.gif").to end_with(".jpg", ".png") }
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
suite.tests.add(
|
|
211
|
+
SmartestSelfTest.test_case(
|
|
212
|
+
"bad negated suffix",
|
|
213
|
+
proc { expect("screenshot.png").not_to end_with(".png") }
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
status, output = SmartestSelfTest.run_suite(suite)
|
|
218
|
+
|
|
219
|
+
expect(status).to eq(1)
|
|
220
|
+
expect(output).to include('expected "https://example.test/path" to start with "about:"')
|
|
221
|
+
expect(output).to include('expected "asset.gif" to end with ".jpg" or ".png"')
|
|
222
|
+
expect(output).to include('expected "screenshot.png" not to end with ".png"')
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
test("supports change matcher for block expectations") do
|
|
226
|
+
value = 0
|
|
227
|
+
action_calls = 0
|
|
228
|
+
|
|
229
|
+
expect {
|
|
230
|
+
action_calls += 1
|
|
231
|
+
value += 2
|
|
232
|
+
}.to change { value }.from(0).to(2).by(2)
|
|
233
|
+
|
|
234
|
+
expect(action_calls).to eq(1)
|
|
235
|
+
expect { value }.not_to change { value }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
test("reports change matcher failures with before and after values") do
|
|
239
|
+
value = 0
|
|
240
|
+
|
|
241
|
+
error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
|
|
242
|
+
expect { value += 1 }.to change { value }.from(0).to(2).by(2)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
expect(error.message).to include("0 before")
|
|
246
|
+
expect(error.message).to include("1 after")
|
|
247
|
+
expect(error.message).to include("to(2)")
|
|
248
|
+
expect(error.message).to include("by(2)")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
test("fails negated change matcher when the value changes") do
|
|
252
|
+
value = 0
|
|
253
|
+
|
|
254
|
+
error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
|
|
255
|
+
expect { value += 1 }.not_to change { value }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
expect(error.message).to include("expected value not to change")
|
|
259
|
+
expect(error.message).to include("0 before")
|
|
260
|
+
expect(error.message).to include("1 after")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
test("requires change matcher value and action blocks") do
|
|
264
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
265
|
+
change
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
expect(error.message).to eq("change requires a block")
|
|
269
|
+
|
|
270
|
+
error = SmartestSelfTest.capture_error(ArgumentError) do
|
|
271
|
+
change(:value) { :other }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
expect(error.message).to include("change does not support arguments")
|
|
275
|
+
|
|
276
|
+
error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
|
|
277
|
+
expect(:value).to change { :other }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
expect(error.message).to include("expected a block to change value")
|
|
281
|
+
end
|
|
282
|
+
|
|
187
283
|
test("registers matcher modules for suite execution contexts") do
|
|
188
284
|
status_matcher = Class.new do
|
|
189
285
|
def initialize(expected)
|