speq 0.2.1 → 0.3.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/Gemfile.lock +1 -1
- data/README.md +312 -20
- data/bin/console +1 -0
- data/exe/speq +2 -3
- data/lib/speq.rb +25 -10
- data/lib/speq/action.rb +97 -0
- data/lib/speq/cli.rb +3 -5
- data/lib/speq/fake.rb +5 -1
- data/lib/speq/matcher.rb +54 -11
- data/lib/speq/report.rb +9 -16
- data/lib/speq/test.rb +10 -1
- data/lib/speq/unit.rb +12 -8
- data/lib/speq/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d4e8796522782ab6e954f384d21d6b6d1d5e6728c773c68e587873f4183173e
|
|
4
|
+
data.tar.gz: f0e4a46ce5889abf250761d4f990a43a61792c6bade4e30e88fd7e9c45869699
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a08b7857d12e9b57af750fb35db681884a7af03a6224424abf659ad2eb4fde5353c8dbb3b873ab6c9f0ed6ac90ae4732338150748def36ddda786ad1921845f
|
|
7
|
+
data.tar.gz: e4ac157417ac3cda5a872b1b13a39ece575d437b10ea8fbca320d71ab383b255ca66be76085ce07bc3f20054d796dacfe17ad9a855632b10b44e21fe67c26681
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
# Speq
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Build specs with fewer words
|
|
4
4
|
|
|
5
|
-
Speq is
|
|
5
|
+
Speq is a testing library for rapid prototyping in Ruby.
|
|
6
6
|
|
|
7
|
-
Speq favors simplicity and minimalism
|
|
7
|
+
Speq favors simplicity and minimalism.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
is(Array.new).empty?
|
|
11
|
+
# Passed (1/1)
|
|
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] )
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
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.
|
|
8
25
|
|
|
9
26
|
## Installation
|
|
10
27
|
|
|
@@ -22,45 +39,320 @@ Or install it yourself as:
|
|
|
22
39
|
|
|
23
40
|
gem install speq
|
|
24
41
|
|
|
25
|
-
## Design
|
|
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.
|
|
26
74
|
|
|
27
|
-
|
|
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).
|
|
28
76
|
|
|
29
|
-
|
|
77
|
+
- message: `does(*:symbol` or `is(*:symbol)`, default: `:itself`
|
|
78
|
+
- arguments: `with(...)` or `of(...)`, default: `*[]`
|
|
79
|
+
- receiver: `on(object, description(optional))`, default: `Object`
|
|
30
80
|
|
|
31
|
-
|
|
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
|
+
```
|
|
32
94
|
|
|
33
|
-
|
|
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)
|
|
34
96
|
|
|
35
97
|
```ruby
|
|
36
|
-
|
|
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].
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Matchers
|
|
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
|
|
157
|
+
|
|
158
|
+
arr = Array.new(length, default)
|
|
159
|
+
speq(arr, 'Either empty, all 1, or all -1').or?.empty?.xor?.include?(-1).include?(1)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### Generated & built-in matchers
|
|
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)
|
|
180
|
+
|
|
181
|
+
# Compound matchers
|
|
182
|
+
either?(*matcher) # matcher1 || matcher2 || ...
|
|
183
|
+
neither?(*matcher) # !matcher1 || !matcher2 || ...
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### Matcher name precedence
|
|
188
|
+
|
|
189
|
+
Parsing the name of a matcher gives highest precedence to the object being tested. For example, consider the following test:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
class Display
|
|
193
|
+
attr_reader :type
|
|
194
|
+
|
|
195
|
+
def initialize(type = :case)
|
|
196
|
+
@type = type.to_sym
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def case?
|
|
200
|
+
@type == :case
|
|
201
|
+
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
|
+
|
|
243
|
+
Fakes are Speq's simple implementation of a test double. Most closely resembling a stub, fakes provide canned or computed responses, allowing for additional test isolation for objects that rely on objects not within the scope of testing.
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
fake_bank = fake(
|
|
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)
|
|
274
|
+
end
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### Sugar
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
# Action-Question Chains
|
|
281
|
+
on('3 2 1')
|
|
282
|
+
<< :split
|
|
283
|
+
<< has?(length: 3)
|
|
284
|
+
<< :map << with(&:to_i)
|
|
285
|
+
<< :reduce << with(&:+)
|
|
286
|
+
<< eq?(6)
|
|
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])
|
|
37
301
|
```
|
|
38
302
|
|
|
39
303
|
## Usage
|
|
40
304
|
|
|
41
|
-
###
|
|
305
|
+
### CLI: Speq Files
|
|
306
|
+
|
|
307
|
+
Speq offers a simple CLI that lets you run tests written in dedicated spec files. This is the preferred way to run speq consistent with the examples shown above.
|
|
308
|
+
|
|
309
|
+
Executing `speq` (or `bundle exec speq` if using bundle) will recursively search the working directory and run all files that end with `_speq.rb`.
|
|
310
|
+
|
|
311
|
+
To run individual files, specify a list of speq file prefixes. For example, to run tests that are within the files `example_speq.rb` and `sample_speq.rb`, simply execute:
|
|
312
|
+
|
|
313
|
+
speq example sample
|
|
314
|
+
|
|
315
|
+
Speq files only `require 'speq'` by default, so any external code being tested will still need to be required.
|
|
316
|
+
|
|
317
|
+
### Anywhere: Speq.test
|
|
318
|
+
|
|
319
|
+
Speq can be used anywhere as long as it is required and all tests are written within a block passed to Speq.test.
|
|
42
320
|
|
|
43
321
|
```ruby
|
|
44
322
|
require 'speq'
|
|
45
323
|
|
|
46
|
-
Speq.test do
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
|
|
324
|
+
Speq.test(self) do | context |
|
|
325
|
+
# You can pass the surrounding context if needed.
|
|
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
|
|
50
332
|
end
|
|
51
333
|
```
|
|
52
334
|
|
|
53
|
-
|
|
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.
|
|
54
336
|
|
|
55
|
-
|
|
337
|
+
```ruby
|
|
338
|
+
require 'speq'
|
|
56
339
|
|
|
57
|
-
|
|
340
|
+
class Exam
|
|
341
|
+
include Speq
|
|
58
342
|
|
|
59
|
-
|
|
343
|
+
def initialize(questions)
|
|
344
|
+
@questions = questions
|
|
345
|
+
end
|
|
60
346
|
|
|
61
|
-
|
|
347
|
+
def grade
|
|
348
|
+
@questions.each do | question |
|
|
349
|
+
speq(question.response, question.to_s).eq?(question.answer)
|
|
350
|
+
end
|
|
62
351
|
|
|
63
|
-
|
|
352
|
+
score
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
```
|
|
64
356
|
|
|
65
357
|
## Contributing
|
|
66
358
|
|
data/bin/console
CHANGED
data/exe/speq
CHANGED
data/lib/speq.rb
CHANGED
|
@@ -4,26 +4,41 @@ require 'speq/matcher'
|
|
|
4
4
|
require 'speq/report'
|
|
5
5
|
require 'speq/unit'
|
|
6
6
|
require 'speq/fake'
|
|
7
|
+
require 'speq/action'
|
|
7
8
|
require 'speq/cli'
|
|
8
9
|
|
|
9
10
|
module Speq
|
|
11
|
+
@tests = [Test.new]
|
|
12
|
+
@descriptions = { Object => nil }
|
|
13
|
+
|
|
14
|
+
def self.test(&block)
|
|
15
|
+
self << Test.new
|
|
16
|
+
module_exec(&block)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.<<(test)
|
|
20
|
+
@tests << test
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.descriptions
|
|
24
|
+
@descriptions
|
|
25
|
+
end
|
|
26
|
+
|
|
10
27
|
module_function
|
|
11
28
|
|
|
12
|
-
def method_missing(
|
|
13
|
-
if
|
|
14
|
-
|
|
29
|
+
def method_missing(method_name, *args, &block)
|
|
30
|
+
if Action.instance_methods.include?(method_name)
|
|
31
|
+
Action.new(@tests.last).send(method_name, *args, &block)
|
|
15
32
|
else
|
|
16
33
|
super
|
|
17
34
|
end
|
|
18
35
|
end
|
|
19
36
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
unit = Unit.new(message, kwargs[:on], kwargs[:with], matcher)
|
|
25
|
-
Report.new([unit]).print_report
|
|
37
|
+
def report
|
|
38
|
+
Report.new(@tests).print_report
|
|
39
|
+
end
|
|
26
40
|
|
|
27
|
-
|
|
41
|
+
def fake(**mapping)
|
|
42
|
+
Fake.new(mapping)
|
|
28
43
|
end
|
|
29
44
|
end
|
data/lib/speq/action.rb
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require 'speq'
|
|
2
|
+
|
|
3
|
+
module Speq
|
|
4
|
+
class Action
|
|
5
|
+
attr_accessor :test_group, :message_queue, :receiver, :arguments_queue
|
|
6
|
+
|
|
7
|
+
def initialize(
|
|
8
|
+
test_group,
|
|
9
|
+
receiver = Object,
|
|
10
|
+
messages = [:itself],
|
|
11
|
+
arguments = [{ args: [], block: nil }]
|
|
12
|
+
)
|
|
13
|
+
@test_group = test_group
|
|
14
|
+
|
|
15
|
+
@message_queue = messages
|
|
16
|
+
@arguments_queue = arguments
|
|
17
|
+
@receiver = receiver
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def method_missing(method, *args, &block)
|
|
21
|
+
if method.to_s.end_with?('?')
|
|
22
|
+
matcher = Matcher.send(method, *args, &block)
|
|
23
|
+
@test_group << Unit.new(clone, matcher)
|
|
24
|
+
else
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def clone
|
|
30
|
+
self.class.new(
|
|
31
|
+
test_group,
|
|
32
|
+
receiver,
|
|
33
|
+
message_queue.clone,
|
|
34
|
+
arguments_queue.clone
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def result
|
|
39
|
+
until @message_queue.empty?
|
|
40
|
+
args = arguments_queue.shift
|
|
41
|
+
message = message_queue.shift
|
|
42
|
+
|
|
43
|
+
@receiver = receiver.send(message, *args[:args], &args[:block])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@receiver
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def on(receiver, description = nil)
|
|
50
|
+
@receiver = receiver
|
|
51
|
+
Speq.descriptions[receiver] = description || receiver
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def does(*messages)
|
|
56
|
+
messages.each do |message|
|
|
57
|
+
message_queue.push(message)
|
|
58
|
+
arguments_queue.push(args: [], block: nil)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def with(*args, &block)
|
|
65
|
+
arguments_queue.last[:args] = args
|
|
66
|
+
arguments_queue.last[:block] = block
|
|
67
|
+
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def format_arguments
|
|
72
|
+
arguments = arguments_queue.last
|
|
73
|
+
argument_description = ''
|
|
74
|
+
|
|
75
|
+
unless arguments[:args].empty?
|
|
76
|
+
argument_description << "with '#{arguments[:args].join(', ')}'"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
argument_description << ' and a block' if arguments[:block]
|
|
80
|
+
|
|
81
|
+
argument_description
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def format_receiver
|
|
85
|
+
Speq.descriptions[receiver] ? "on '#{Speq.descriptions[receiver]}'" : ''
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_s
|
|
89
|
+
[message_queue.last, format_arguments, format_receiver]
|
|
90
|
+
.reject(&:empty?)
|
|
91
|
+
.join(' ')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
alias is does
|
|
95
|
+
alias of with
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/speq/cli.rb
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
require 'speq'
|
|
1
2
|
require 'find'
|
|
2
3
|
require 'colorize'
|
|
3
4
|
|
|
4
5
|
# Provides a CLI for running Speq
|
|
5
6
|
module Speq
|
|
6
7
|
class CLI
|
|
7
|
-
attr_accessor :tests
|
|
8
|
-
|
|
9
8
|
def initialize(cli_args)
|
|
10
9
|
@files = find_files(cli_args)
|
|
11
|
-
|
|
10
|
+
run
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
def find_files(file_prefixes)
|
|
@@ -19,8 +18,7 @@ module Speq
|
|
|
19
18
|
|
|
20
19
|
def run
|
|
21
20
|
@files.each { |file| Speq.module_eval(File.read(file), file) }
|
|
22
|
-
|
|
23
|
-
@tests.all?(&:passed?)
|
|
21
|
+
Speq.report
|
|
24
22
|
end
|
|
25
23
|
end
|
|
26
24
|
end
|
data/lib/speq/fake.rb
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# The Fake class
|
|
1
|
+
# The Fake class is a lightweight, general-purpose test double
|
|
2
2
|
module Speq
|
|
3
3
|
class Fake
|
|
4
4
|
def initialize(mapping)
|
|
@@ -14,5 +14,9 @@ module Speq
|
|
|
14
14
|
-> { return_value }
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
|
+
|
|
18
|
+
def to_s
|
|
19
|
+
description = send(:class) == self.class ? 'a fake' : "a fake#{send(:class)}"
|
|
20
|
+
end
|
|
17
21
|
end
|
|
18
22
|
end
|
data/lib/speq/matcher.rb
CHANGED
|
@@ -2,31 +2,74 @@
|
|
|
2
2
|
# respond to match, returning true for an expected return value of a unit test
|
|
3
3
|
module Speq
|
|
4
4
|
class Matcher
|
|
5
|
-
def initialize(
|
|
6
|
-
@
|
|
5
|
+
def initialize(expectation, phrase)
|
|
6
|
+
@expectation = expectation
|
|
7
|
+
@phrase = phrase
|
|
7
8
|
end
|
|
8
9
|
|
|
9
|
-
def match(actual)
|
|
10
|
-
@
|
|
10
|
+
def match?(actual)
|
|
11
|
+
@actual = actual
|
|
12
|
+
@expectation[actual]
|
|
11
13
|
end
|
|
12
14
|
|
|
13
|
-
def
|
|
15
|
+
def to_s
|
|
16
|
+
@phrase[@actual]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.method_missing(method_name, *args, &block)
|
|
20
|
+
if method_name.to_s.end_with?('?')
|
|
21
|
+
dynamic_matcher(method_name, *args, &block)
|
|
22
|
+
|
|
23
|
+
else
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.dynamic_matcher(method_name, *args, &block)
|
|
29
|
+
Matcher.new(
|
|
30
|
+
->(object) { object.send(method_name, *args, &block) },
|
|
31
|
+
proc { "passes: #{method_name}" }
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.pass?(&prc)
|
|
36
|
+
Matcher.new(prc)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.true?
|
|
40
|
+
Matcher.eq?(true)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.truthy?
|
|
14
44
|
Matcher.new(->(actual_value) { actual_value ? true : false })
|
|
15
45
|
end
|
|
16
46
|
|
|
17
|
-
def self.
|
|
47
|
+
def self.falsy?
|
|
18
48
|
Matcher.new(->(actual_value) { actual_value ? false : true })
|
|
19
49
|
end
|
|
20
50
|
|
|
21
|
-
def self.eq(expected_value)
|
|
22
|
-
Matcher.new(
|
|
51
|
+
def self.eq?(expected_value)
|
|
52
|
+
Matcher.new(
|
|
53
|
+
->(actual_value) { expected_value.eql?(actual_value) },
|
|
54
|
+
->(actual_value) { "equals #{actual_value}" }
|
|
55
|
+
)
|
|
23
56
|
end
|
|
24
57
|
|
|
25
|
-
def self.
|
|
26
|
-
|
|
58
|
+
def self.have?(*symbols, **key_value_pairs)
|
|
59
|
+
new(
|
|
60
|
+
lambda do |object|
|
|
61
|
+
symbols.each { |symbol| return false unless object.respond_to?(symbol) }
|
|
62
|
+
key_value_pairs.each { |key, value| return false unless object.send(key) == value }
|
|
63
|
+
end,
|
|
64
|
+
proc do
|
|
65
|
+
properties = symbols.empty? ? '' : symbols.join(' ') + ', '
|
|
66
|
+
values = key_value_pairs.map { |key, value| "#{key}: #{value}" }.join(' ')
|
|
67
|
+
"has #{properties}#{values}"
|
|
68
|
+
end
|
|
69
|
+
)
|
|
27
70
|
end
|
|
28
71
|
|
|
29
|
-
def self.
|
|
72
|
+
def self.raise?(expected_except)
|
|
30
73
|
case expected_except
|
|
31
74
|
when Class
|
|
32
75
|
raise_class(expected_except)
|
data/lib/speq/report.rb
CHANGED
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
# The Report class produces and prints a report per test group
|
|
2
2
|
module Speq
|
|
3
3
|
class Report
|
|
4
|
-
def initialize(
|
|
5
|
-
@units = units
|
|
4
|
+
def initialize(tests)
|
|
5
|
+
@units = tests.flat_map(&:units)
|
|
6
|
+
@passed = []
|
|
7
|
+
@failed = []
|
|
6
8
|
end
|
|
7
9
|
|
|
8
10
|
def print_report
|
|
9
|
-
report_string = ''
|
|
10
|
-
|
|
11
11
|
@units.each do |unit|
|
|
12
12
|
if unit.passed?
|
|
13
|
-
|
|
14
|
-
color = :green
|
|
13
|
+
@passed << unit.to_s.colorize(:green)
|
|
15
14
|
else
|
|
16
|
-
|
|
17
|
-
color = :red
|
|
15
|
+
@failed << unit.to_s.colorize(:red)
|
|
18
16
|
end
|
|
19
|
-
|
|
20
|
-
method = "calling '#{unit.message}' "
|
|
21
|
-
arguments = unit.arguments ? "with arguments: '#{unit.arguments}'" : ''
|
|
22
|
-
receiver = unit.receiver == Object ? '' : "on: #{unit.receiver} "
|
|
23
|
-
|
|
24
|
-
report_string <<
|
|
25
|
-
[outcome, method, arguments, receiver, "\n"].join.colorize(color)
|
|
26
17
|
end
|
|
27
18
|
|
|
28
|
-
puts
|
|
19
|
+
puts "Passed (#{@passed.length}/#{@units.length})" unless @passed.empty?
|
|
20
|
+
puts @passed
|
|
21
|
+
puts "Failed (#{@failed.length}/#{@units.length})" unless @failed.empty?
|
|
29
22
|
end
|
|
30
23
|
end
|
|
31
24
|
end
|
data/lib/speq/test.rb
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
require 'speq/action'
|
|
2
|
+
|
|
3
|
+
# A
|
|
2
4
|
module Speq
|
|
3
5
|
class Test
|
|
6
|
+
attr_reader :units
|
|
7
|
+
|
|
4
8
|
def initialize
|
|
5
9
|
@units = []
|
|
6
10
|
end
|
|
@@ -8,5 +12,10 @@ module Speq
|
|
|
8
12
|
def passed?
|
|
9
13
|
@units.all?(&:passed?)
|
|
10
14
|
end
|
|
15
|
+
|
|
16
|
+
def <<(unit)
|
|
17
|
+
@units << unit
|
|
18
|
+
self
|
|
19
|
+
end
|
|
11
20
|
end
|
|
12
21
|
end
|
data/lib/speq/unit.rb
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
|
+
require 'speq/action'
|
|
2
|
+
|
|
1
3
|
# The Unit class is responsible for running a test and storing the result
|
|
2
4
|
module Speq
|
|
3
5
|
class Unit
|
|
4
|
-
attr_reader :
|
|
6
|
+
attr_reader :result, :action, :matcher
|
|
5
7
|
|
|
6
|
-
def initialize(
|
|
7
|
-
@
|
|
8
|
+
def initialize(action, matcher)
|
|
9
|
+
@action = action
|
|
10
|
+
@matcher = matcher
|
|
11
|
+
@result = matcher.match?(action.clone.result)
|
|
8
12
|
rescue StandardError => exception
|
|
9
|
-
@result = matcher.match(exception)
|
|
10
|
-
ensure
|
|
11
|
-
@message = message
|
|
12
|
-
@receiver = receiver
|
|
13
|
-
@arguments = arguments
|
|
13
|
+
@result = matcher.match?(exception)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def passed?
|
|
17
17
|
@result
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
def to_s
|
|
21
|
+
"#{action} #{matcher}"
|
|
22
|
+
end
|
|
19
23
|
end
|
|
20
24
|
end
|
data/lib/speq/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: speq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- zaniar moradian
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2018-
|
|
11
|
+
date: 2018-11-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -84,6 +84,7 @@ files:
|
|
|
84
84
|
- bin/setup
|
|
85
85
|
- exe/speq
|
|
86
86
|
- lib/speq.rb
|
|
87
|
+
- lib/speq/action.rb
|
|
87
88
|
- lib/speq/cli.rb
|
|
88
89
|
- lib/speq/fake.rb
|
|
89
90
|
- lib/speq/matcher.rb
|