speq 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/.travis.yml +2 -2
- data/README.md +40 -290
- data/bin/console +3 -3
- data/exe/speq +5 -1
- data/lib/speq/cli.rb +34 -13
- data/lib/speq/question.rb +162 -0
- data/lib/speq/recruit.rb +86 -0
- data/lib/speq/string_fmt.rb +93 -0
- data/lib/speq/values.rb +130 -0
- data/lib/speq/version.rb +3 -1
- data/lib/speq.rb +150 -29
- data/speq.gemspec +6 -2
- metadata +14 -17
- data/Gemfile.lock +0 -28
- data/lib/speq/action.rb +0 -97
- data/lib/speq/fake.rb +0 -22
- data/lib/speq/matcher.rb +0 -93
- data/lib/speq/report.rb +0 -24
- data/lib/speq/test.rb +0 -21
- data/lib/speq/unit.rb +0 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: efc22fe2da50ca215a4ca412a768e04e70fe97ff535d50ab8bd38cf918676fbf
|
|
4
|
+
data.tar.gz: 2daeaccd796d8eaa2fd8369495675ffd7df8171fb7bdaed2529a781dfeb4544d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c6ed8070160429f4fa4badfda408e09a5d95900f5df0b6a3014c09a85496aa5ca6baca3efb76d0bff9b2ff9211b234133512df8d300ab833b4bd707831b53479
|
|
7
|
+
data.tar.gz: c61f81ead1d0e33220cd0e0697376a28cb4be9db73156670d4ab1d945cb06c491e13b27d105535470c552428f27a0b7eeaebfeb99963b565269026709790486b
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.6.5
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Speq
|
|
2
2
|
|
|
3
|
+
**Ready to use! Issue reports are welcome!**
|
|
4
|
+
|
|
3
5
|
## Build specs with fewer words
|
|
4
6
|
|
|
5
7
|
Speq is a testing library for rapid prototyping in Ruby.
|
|
@@ -7,18 +9,8 @@ Speq is a testing library for rapid prototyping in Ruby.
|
|
|
7
9
|
Speq favors simplicity and minimalism.
|
|
8
10
|
|
|
9
11
|
```ruby
|
|
10
|
-
is(
|
|
11
|
-
#
|
|
12
|
-
# [] is empty.
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
But also values flexibility.
|
|
16
|
-
|
|
17
|
-
```ruby
|
|
18
|
-
speq(Array(nil), 'an array created by calling Array(nil)').eq?([nil])
|
|
19
|
-
|
|
20
|
-
# Failed (1/1)
|
|
21
|
-
# An array created by calling Array(nil) is NOT equal to [nil]. ( [] != [nil] )
|
|
12
|
+
is([]).empty?
|
|
13
|
+
# ✔ [] is empty.
|
|
22
14
|
```
|
|
23
15
|
|
|
24
16
|
Speq is ideal whenever it would feel excessive to use one of the existing frameworks, but it's not enough to just print & inspect outputs.
|
|
@@ -33,271 +25,55 @@ gem 'speq'
|
|
|
33
25
|
|
|
34
26
|
And then execute:
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Or install it yourself as:
|
|
39
|
-
|
|
40
|
-
gem install speq
|
|
41
|
-
|
|
42
|
-
## Design & Syntax
|
|
43
|
-
|
|
44
|
-
Speq is loosely based on the given-when-then or arrange-act-assert testing pattern. Speq's design choices are guided by the competing motivations of having tests be as short and simple as possible while maintaining the flexibility to be as descriptive as needed.
|
|
45
|
-
|
|
46
|
-
In contrast to similar tools, speqs are typically framed as _questions_ about a program's behavior rather than assertions about the desired behavior. The most basic test involves passing a string to the method `speq` followed by `pass?` and a block that evaluates strictly to true or false.
|
|
47
|
-
|
|
48
|
-
```ruby
|
|
49
|
-
speq("Does Ruby properly calculate Euler's identity?").pass? do
|
|
50
|
-
e = Math::E
|
|
51
|
-
π = Math::PI
|
|
52
|
-
i = (1i)
|
|
53
|
-
|
|
54
|
-
e**(i*π) + 1 == 0
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Passed (1/1)
|
|
58
|
-
# Testing 'does Ruby properly calculate Euler's identity?' passes.
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
The above works, but it's not much shorter than existing solutions. More importantly, it's missing vital information about what action was taken, what the outcome was, and what assertion led to the success.
|
|
62
|
-
|
|
63
|
-
Shown below is the basic two-part pattern for most speqs and the resulting report combining explicit and implicit descriptions:
|
|
64
|
-
|
|
65
|
-
```ruby
|
|
66
|
-
speq(Math::E**((1i) * Math::PI ) + 1, 'e^iπ + 1').eq?(0)
|
|
67
|
-
# Passed (1/1)
|
|
68
|
-
# e^iπ + 1 is equal to 0. ( (0.0+0.0i) == 0 )
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Actions
|
|
72
|
-
|
|
73
|
-
Descriptions for simple unit tests are often closely tied to the source code, and as such, Speq tries to eliminate them wherever possible. To automatically generate descriptions, the action or subject of interest is supplied to be evaluated dynamically.
|
|
74
|
-
|
|
75
|
-
More specifically, we begin by setting up the program state and then explicitly supply the action being tested using the methods below. By breaking down the expression's method/message, arguments, and receiver, Speq can use the additional information to help generate an often sufficiently detailed description of the test. Actions are evaluated when they reach a [matcher](#matchers).
|
|
76
|
-
|
|
77
|
-
- message: `does(*:symbol` or `is(*:symbol)`, default: `:itself`
|
|
78
|
-
- arguments: `with(...)` or `of(...)`, default: `*[]`
|
|
79
|
-
- receiver: `on(object, description(optional))`, default: `Object`
|
|
80
|
-
|
|
81
|
-
```ruby
|
|
82
|
-
is(:prime?).of(2).true?
|
|
83
|
-
|
|
84
|
-
on((1..4).to_a.reverse).does(:sort).eq?([1, 2, 3, 4])
|
|
85
|
-
|
|
86
|
-
# .with() accepts arguments with the exact same format as any other method
|
|
87
|
-
does(:map).with { |idx| idx * idx }.on(0..3).eq?([0, 1, 4, 9])
|
|
88
|
-
|
|
89
|
-
# Passed (3/3)
|
|
90
|
-
# prime? of 2 is true.
|
|
91
|
-
# sort on [4, 3, 2, 1] equals [1, 2, 3, 4].
|
|
92
|
-
# map with a block on 1..4 equals [0, 1, 4, 9].
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Chaining methods can be accomplished by providing multiple symbols. If an intermediate value needs to be tested or a method needs to be given arguments, this can be accomplished by using `.then(:next_method).with`. An example of this is shown below. An alternative, unconventional syntax for accomplishing the same can be found in the section on [syntactic sugar](#Sugar)
|
|
96
|
-
|
|
97
|
-
```ruby
|
|
98
|
-
on(1..4)
|
|
99
|
-
.does(:to_a, :shuffle, :sort)
|
|
100
|
-
.eq?([1, 2, 3, 4])
|
|
101
|
-
.then(:sort).with { | a, b | b <=> a }
|
|
102
|
-
.eq?([4, 3, 2, 1])
|
|
103
|
-
|
|
104
|
-
# Passed (1/1)
|
|
105
|
-
# to_a on 1..4 then reverse (on [1, 2, 3, 4]) then sort (on [4, 3, 2, 1]) equals [1, 2, 3, 4].
|
|
28
|
+
```sh
|
|
29
|
+
bundle
|
|
106
30
|
```
|
|
107
31
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
All speq methods that end with a question mark are matchers.
|
|
111
|
-
|
|
112
|
-
The matcher `pass?` is the most general and simply expects to be given a code block that evaluates to true or false. The output from the action is passed along as the first piped variable.
|
|
113
|
-
|
|
114
|
-
```ruby
|
|
115
|
-
does(:strip)
|
|
116
|
-
.on(' speq ')
|
|
117
|
-
.pass? { |str| str == 'speq' && !str.include?(' ') }
|
|
118
|
-
|
|
119
|
-
does(:rand)
|
|
120
|
-
.pass? { |val| val.is_a?(Float) }
|
|
121
|
-
|
|
122
|
-
does(:prime?)
|
|
123
|
-
.with(-1)
|
|
124
|
-
.raise?('Prime is not defined for negative numbers')
|
|
125
|
-
|
|
126
|
-
on(User)
|
|
127
|
-
.does(:new)
|
|
128
|
-
.with(id: 1, name: 'user name')
|
|
129
|
-
.have?(:id)
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
#### Prefixes and Compound Matchers
|
|
133
|
-
|
|
134
|
-
Multiple matchers can follow an action. By default, all matchers must pass for the test to be considered successful. To run multiple tests on the same action, see the section on [test subjects](#subjects)
|
|
135
|
-
|
|
136
|
-
The logical inverse of any matcher can be achieved by prefixing the matcher with `not_`.
|
|
137
|
-
|
|
138
|
-
```ruby
|
|
139
|
-
# Equivalent to the previous speq
|
|
140
|
-
does(:strip).on(' speq ').not_include?(' ').eq?('speq')
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
For simple combinations, it's sufficient to prefix the second matcher with one of the following: `or_, nand_, nor_, xor_, xnor_`
|
|
144
|
-
|
|
145
|
-
```ruby
|
|
146
|
-
arr = Array.new(10, rand.round)
|
|
147
|
-
speq(arr, '10 element array filled with zeros or ones').has?(length: 100).all?(0).or_all?(1)
|
|
148
|
-
|
|
149
|
-
# arr.length == 100 && (arr.all?(0) || arr.all?(1))
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Although it's best to avoid complex combinations of matchers, logical operators can be combined unambiguously if absolutely needed by chaining operators with prefix notation.
|
|
153
|
-
|
|
154
|
-
```ruby
|
|
155
|
-
default = rand.round(1) <=> 0.5
|
|
156
|
-
length = default.zero? ? 0 : 100
|
|
32
|
+
Or install it yourself as:
|
|
157
33
|
|
|
158
|
-
|
|
159
|
-
|
|
34
|
+
```sh
|
|
35
|
+
gem install speq
|
|
160
36
|
```
|
|
161
37
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
If the object being tested responds to the method ending with `?`, then an appropriate matcher is automatically generated. As a result, existing methods such as `instance_of?`, `nil?`, or `empty?` can be used despite the fact that they are not in the list below.
|
|
165
|
-
|
|
166
|
-
```ruby
|
|
167
|
-
pass?(*description, &block)
|
|
168
|
-
has?(*:message, **(message: val)) # alias: have?
|
|
169
|
-
|
|
170
|
-
eq?(obj) # result == obj
|
|
171
|
-
case?(obj) # obj === result
|
|
172
|
-
|
|
173
|
-
true?
|
|
174
|
-
false?
|
|
175
|
-
truthy?
|
|
176
|
-
falsy?
|
|
177
|
-
|
|
178
|
-
raise?('The following error message')
|
|
179
|
-
raise?(ErrorClass)
|
|
38
|
+
## Design & Syntax
|
|
180
39
|
|
|
181
|
-
|
|
182
|
-
either?(*matcher) # matcher1 || matcher2 || ...
|
|
183
|
-
neither?(*matcher) # !matcher1 || !matcher2 || ...
|
|
40
|
+
Speq's design choices are guided by the competing motivations of having tests be as short and simple as possible while maintaining the flexibility to be as descriptive as needed. In contrast to similar tools, speqs are typically framed as _questions_ about a program's behavior rather than assertions about the desired behavior.
|
|
184
41
|
|
|
185
|
-
|
|
42
|
+
Descriptions for simple unit tests are often closely tied to the source code, and as such, Speq tries to eliminate them wherever possible. By breaking down an expression's method/message, arguments, and receiver, Speq can use the additional information to help generate an often sufficiently detailed description of the test.
|
|
186
43
|
|
|
187
|
-
|
|
44
|
+
- receiver: `on(object [, description])`
|
|
45
|
+
- message: `does(symbol [, description])`
|
|
46
|
+
- arguments: `with(*args , &block)` or `of(*args, &block)`
|
|
47
|
+
- question: `*?(*args, &block)`
|
|
188
48
|
|
|
189
|
-
|
|
49
|
+
Note that Speq executes expressions immediately, avoiding the unintuitive execution order of most testing libraries.
|
|
190
50
|
|
|
191
51
|
```ruby
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def initialize(type = :case)
|
|
196
|
-
@type = type.to_sym
|
|
197
|
-
end
|
|
52
|
+
speq('Example test') do
|
|
53
|
+
on((1..4).to_a.shuffle, 'a shuffled array').does(:sort).eq?([1, 2, 3, 4])
|
|
198
54
|
|
|
199
|
-
|
|
200
|
-
|
|
55
|
+
does(:reverse) do
|
|
56
|
+
on('3 2 1').eq?('1 2 3')
|
|
57
|
+
on([3, 2, 1]).eq?([1, 2, 3])
|
|
201
58
|
end
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
window_display = Display.new('window')
|
|
205
|
-
|
|
206
|
-
speq(window_display).not_case? # Will use Display#case
|
|
207
|
-
on(window_display).is(:type).case?(Symbol) # Will use Speq#case?
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
To resolve `not_case?`, speq will do the following:
|
|
211
|
-
|
|
212
|
-
1. Checks for the prefix `speq_`, reserved in case of name conflicts.
|
|
213
|
-
2. Generates a matcher if the object responds to `not_case?`.
|
|
214
|
-
3. Generates a matcher if the object responds to `case?` and applies prefixes.
|
|
215
|
-
4. Uses the built-in `case?` and applies prefixes.
|
|
216
|
-
|
|
217
|
-
### Additional Functionality
|
|
218
|
-
|
|
219
|
-
#### Object Descriptions
|
|
220
|
-
|
|
221
|
-
It may be advantageous to describe an object in advance such that, from there on, the report will automatically include the object description anytime that object is encountered.
|
|
222
|
-
|
|
223
|
-
```ruby
|
|
224
|
-
MyClass = Class.new
|
|
225
|
-
|
|
226
|
-
call(MyClass, 'my class, an instance of the class Class')
|
|
227
|
-
speq(MyClass).instance_of?(Class)
|
|
228
|
-
|
|
229
|
-
# call tracks both object identity and object equality
|
|
230
|
-
my_arr = []
|
|
231
|
-
call(my_arr, 'a specific empty array')
|
|
232
|
-
call([], 'an empty array')
|
|
233
|
-
|
|
234
|
-
speq(my_arr).eq?([])
|
|
235
|
-
|
|
236
|
-
# Passed (2/2)
|
|
237
|
-
# 'my class, an instance of the class Class' is an instance of the class Class.
|
|
238
|
-
# 'my specific empty array' equals 'an empty array'
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
#### Fakes
|
|
242
59
|
|
|
243
|
-
|
|
60
|
+
with(&->(idx) { idx * idx }).does(:map).on(0..3).eq?([0, 1, 4, 9])
|
|
244
61
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
balance: 5000,
|
|
248
|
-
print_balance: '$50.00',
|
|
249
|
-
withdraw: proc {|amount| [50, amount].min }
|
|
250
|
-
)
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
#### Subjects
|
|
254
|
-
|
|
255
|
-
Speq has the distinct advantage of making it particularly easy to run many similar tests on the same subject.
|
|
256
|
-
Opening a block on an action that has not been clo
|
|
257
|
-
|
|
258
|
-
```ruby
|
|
259
|
-
not_prime = [0, 1, 4, 6, 8, 9]
|
|
260
|
-
prime = [2, 3, 5, 7]
|
|
261
|
-
|
|
262
|
-
#
|
|
263
|
-
does(:prime?) do
|
|
264
|
-
with[not_prime].false?
|
|
265
|
-
with[prime].true?
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
does(:my_sort) do
|
|
269
|
-
small_array = [3, 2, 1]
|
|
270
|
-
large_array = Array.new(10**6) {rand}
|
|
271
|
-
|
|
272
|
-
on(small_array).eq?(small_array.sort)
|
|
273
|
-
on(large_array).eq?(large_array.sort)
|
|
62
|
+
is(:rand).a?(Float) # symbol -> does
|
|
63
|
+
is([]).empty? # not symbol -> on
|
|
274
64
|
end
|
|
275
65
|
```
|
|
276
66
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
# Can start with the usual syntax and switch when convenient
|
|
289
|
-
on(1..4).does(:to_a, :shuffle, :sort).eq?([1, 2, 3, 4])
|
|
290
|
-
<< :sort << with { | a, b | b <=> a } << eq?([4, 3, 2, 1])
|
|
291
|
-
|
|
292
|
-
# Broadcasting
|
|
293
|
-
def add(a, b)
|
|
294
|
-
a + b
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
a = [1, 2, 3]
|
|
298
|
-
b = [4, 5, 6]
|
|
299
|
-
|
|
300
|
-
does(:add).with[a, b].eq?([5, 7, 9])
|
|
67
|
+
```
|
|
68
|
+
✔ Example test
|
|
69
|
+
✔ sort on a shuffled array equals [1, 2, 3, 4].
|
|
70
|
+
✔ reverse...
|
|
71
|
+
✔ "3 2 1" equals "1 2 3".
|
|
72
|
+
✔ [3, 2, 1] equals [1, 2, 3].
|
|
73
|
+
✔ map(&{ ... }) on 0..3 equals [0, 1, 4, 9].
|
|
74
|
+
✔ rand is a Float.
|
|
75
|
+
✔ [] is empty.
|
|
76
|
+
pass: 5, fail: 0, errors: 0
|
|
301
77
|
```
|
|
302
78
|
|
|
303
79
|
## Usage
|
|
@@ -312,7 +88,7 @@ To run individual files, specify a list of speq file prefixes. For example, to r
|
|
|
312
88
|
|
|
313
89
|
speq example sample
|
|
314
90
|
|
|
315
|
-
|
|
91
|
+
Note that you don't need to require 'speq' within a speq file if running from the CLI.
|
|
316
92
|
|
|
317
93
|
### Anywhere: Speq.test
|
|
318
94
|
|
|
@@ -321,37 +97,11 @@ Speq can be used anywhere as long as it is required and all tests are written wi
|
|
|
321
97
|
```ruby
|
|
322
98
|
require 'speq'
|
|
323
99
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
# Calling 'report' prints the tests seen so far to $stdout.
|
|
327
|
-
|
|
328
|
-
# By default, Speq.test Returns the instance of Speq::Test created here.
|
|
329
|
-
# Using the return keyword, you can choose to return the following instead:
|
|
330
|
-
# 'return score': returns the proportion of tests passed as a Rational number
|
|
331
|
-
# 'return pass?': returns true or false for whether all the tests passed
|
|
332
|
-
end
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
Speq can also be included to be used in a more direct manner. Here is an example of using Speq within another class for added functionality.
|
|
336
|
-
|
|
337
|
-
```ruby
|
|
338
|
-
require 'speq'
|
|
339
|
-
|
|
340
|
-
class Exam
|
|
341
|
-
include Speq
|
|
342
|
-
|
|
343
|
-
def initialize(questions)
|
|
344
|
-
@questions = questions
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
def grade
|
|
348
|
-
@questions.each do | question |
|
|
349
|
-
speq(question.response, question.to_s).eq?(question.answer)
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
score
|
|
353
|
-
end
|
|
100
|
+
speq("testing anywhere") do
|
|
101
|
+
...
|
|
354
102
|
end
|
|
103
|
+
# '.score': returns the proportion of tests passed as a Rational number
|
|
104
|
+
# '.pass?': returns true or false for whether all the tests passed
|
|
355
105
|
```
|
|
356
106
|
|
|
357
107
|
## Contributing
|
data/bin/console
CHANGED
data/exe/speq
CHANGED
data/lib/speq/cli.rb
CHANGED
|
@@ -1,24 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'find'
|
|
3
|
-
require '
|
|
4
|
+
require 'date'
|
|
5
|
+
require 'pry'
|
|
4
6
|
|
|
5
|
-
# Provides a CLI for running Speq
|
|
6
7
|
module Speq
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# Includes useful methods for running Speq from a CLI
|
|
9
|
+
module CLI
|
|
10
|
+
def self.run(cli_args)
|
|
11
|
+
run_tests(find_files(cli_args))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.run_and_report(cli_args)
|
|
15
|
+
print_report(run(cli_args))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.run_tests(paths)
|
|
19
|
+
paths.map { |file| file_test(file) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.file_test(file)
|
|
23
|
+
speq(file_description(file)) { instance_eval(File.read(file), file) }
|
|
11
24
|
end
|
|
12
25
|
|
|
13
|
-
def
|
|
14
|
-
|
|
26
|
+
def self.file_description(path)
|
|
27
|
+
path
|
|
28
|
+
.split('/').last
|
|
29
|
+
.delete_suffix('_speq.rb')
|
|
30
|
+
.split('_')
|
|
31
|
+
.map(&:capitalize)
|
|
32
|
+
.join(' ')
|
|
33
|
+
end
|
|
15
34
|
|
|
16
|
-
|
|
35
|
+
def self.find_files(cli_args)
|
|
36
|
+
prefixes = cli_args.empty? ? ['*'] : cli_args
|
|
37
|
+
glob_pattern = "#{Dir.pwd}/**/{#{prefixes.join(',')}}_speq.rb"
|
|
38
|
+
Dir.glob(glob_pattern)
|
|
17
39
|
end
|
|
18
40
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
Speq.report
|
|
41
|
+
def self.print_report(tests)
|
|
42
|
+
puts tests
|
|
22
43
|
end
|
|
23
44
|
end
|
|
24
45
|
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'speq/values'
|
|
4
|
+
|
|
5
|
+
module Speq
|
|
6
|
+
# Error class for Question
|
|
7
|
+
class QuestionError < StandardError
|
|
8
|
+
def initialize(question_name, error)
|
|
9
|
+
super("#{question_name}: #{error}")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Records the question name, result, and constructs the corresponding matcher
|
|
14
|
+
class Question
|
|
15
|
+
def self.question?(method_name)
|
|
16
|
+
method_name.to_s.end_with?('?')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :question_name, :matcher, :result
|
|
20
|
+
|
|
21
|
+
def initialize(question_name, result, matcher)
|
|
22
|
+
@question_name = question_name
|
|
23
|
+
@result = result
|
|
24
|
+
@matcher = matcher
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def phrase
|
|
28
|
+
matcher.phrase(result)
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
QuestionError.new(@question_name, e.inspect).to_s
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def outcome
|
|
34
|
+
matcher.match?(result) ? Outcome.pass : Outcome.fail
|
|
35
|
+
rescue StandardError
|
|
36
|
+
Outcome.error
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.for(result, question_name, *args, &block)
|
|
40
|
+
matcher = Matcher.for(question_name, *args, &block)
|
|
41
|
+
|
|
42
|
+
Question.new(
|
|
43
|
+
question_name,
|
|
44
|
+
result,
|
|
45
|
+
matcher
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Boolean #match? for a given result and descriptive phrase
|
|
51
|
+
class Matcher
|
|
52
|
+
def initialize(matcher, pass_phrase, fail_phrase)
|
|
53
|
+
@matcher = matcher
|
|
54
|
+
@pass_phrase = pass_phrase
|
|
55
|
+
@fail_phrase = fail_phrase
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def match?(actual)
|
|
59
|
+
@matcher[actual]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def phrase(actual)
|
|
63
|
+
@matcher[actual] ? @pass_phrase : @fail_phrase
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.matcher_is(match, thing)
|
|
67
|
+
new(match, "is #{thing}", "is not #{thing}")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.for(question_name, *args, &block)
|
|
71
|
+
if respond_to?(question_name)
|
|
72
|
+
send(question_name, *args, &block)
|
|
73
|
+
else
|
|
74
|
+
result_matcher(question_name, *args, &block)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.match?(description, &block)
|
|
79
|
+
new(block, "matches #{description}", "does not match #{description}")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.result_matcher(question_name, *args, &block)
|
|
83
|
+
new(
|
|
84
|
+
lambda do |obj|
|
|
85
|
+
if obj.respond_to?(question_name)
|
|
86
|
+
return obj.send(question_name, *args, &block)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
raise "No question called #{question_name.inspect} or existing method on #{obj.inspect}"
|
|
90
|
+
end,
|
|
91
|
+
"is #{question_name[0..-2]}",
|
|
92
|
+
"is not #{question_name[0..-2]}"
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.eq?(expected_value)
|
|
97
|
+
new(
|
|
98
|
+
->(result) { expected_value == result },
|
|
99
|
+
"equals #{expected_value.inspect}",
|
|
100
|
+
"does not equal #{expected_value.inspect}"
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.true?
|
|
105
|
+
matcher_is(->(result) { result == true }, 'true')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.truthy?
|
|
109
|
+
matcher_is(->(actual_value) { actual_value ? true : false }, 'truthy')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.falsy?
|
|
113
|
+
matcher_is(->(actual_value) { actual_value ? false : true }, 'falsey')
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.a?(type)
|
|
117
|
+
matcher_is(->(val) { val.is_a?(type) }, "a #{type}")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def self.have?(*symbols, **key_value_pairs)
|
|
121
|
+
new(
|
|
122
|
+
lambda do |object|
|
|
123
|
+
symbols.each do |symbol|
|
|
124
|
+
return false unless object.respond_to?(symbol)
|
|
125
|
+
end
|
|
126
|
+
key_value_pairs.each do |key, value|
|
|
127
|
+
return false unless object.send(key) == value
|
|
128
|
+
end
|
|
129
|
+
end,
|
|
130
|
+
"has #{symbols.empty? ? nil : symbols}#{key_value_pairs}",
|
|
131
|
+
"doesn't have #{symbols.empty? ? nil : symbols}#{key_value_pairs}"
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.raise?(expected_except)
|
|
136
|
+
case expected_except
|
|
137
|
+
when Class
|
|
138
|
+
raise_class(expected_except)
|
|
139
|
+
when String
|
|
140
|
+
raise_message(expected_except)
|
|
141
|
+
else
|
|
142
|
+
raise ArgumentError
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.raise_class(expected_error)
|
|
147
|
+
Matcher.new(
|
|
148
|
+
->(actual_except) { actual_except.class <= expected_error },
|
|
149
|
+
"raises #{expected_error}",
|
|
150
|
+
"does not raise #{expected_error}"
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.raise_message(expected_message)
|
|
155
|
+
Matcher.new(
|
|
156
|
+
->(actual_except) { actual_except.message == expected_message },
|
|
157
|
+
"raises #{expected_message.inspect}",
|
|
158
|
+
"does not raise #{expected_message.inspect}"
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|