rspec 0.5.3 → 0.5.4
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +57 -32
- data/EXAMPLES.rd +0 -0
- data/Rakefile +22 -21
- data/bin/spec +9 -11
- data/doc/README +1 -3
- data/doc/plugin/syntax.rb +27 -5
- data/doc/src/core_team.page +22 -0
- data/doc/src/default.css +11 -11
- data/doc/src/default.template +0 -1
- data/doc/src/documentation/index.page +183 -8
- data/doc/src/documentation/meta.info +7 -7
- data/doc/src/documentation/mocks.page +168 -109
- data/doc/src/documentation/underscores.page +20 -0
- data/doc/src/examples.page +2 -1
- data/doc/src/images/David_and_Aslak.jpg +0 -0
- data/doc/src/images/Whats_That_Dude.jpg +0 -0
- data/doc/src/index.page +70 -3
- data/doc/src/meta.info +18 -11
- data/doc/src/tools/index.page +40 -134
- data/doc/src/tools/meta.info +9 -3
- data/doc/src/tools/rails.page +3 -1
- data/doc/src/tools/rake.page +20 -3
- data/doc/src/tools/rcov.page +19 -0
- data/doc/src/tools/spec.page +99 -0
- data/doc/src/tools/test2rspec.page +2 -4
- data/doc/src/tutorials/index.page +52 -0
- data/doc/src/tutorials/meta.info +31 -0
- data/doc/src/tutorials/notes.txt +252 -0
- data/doc/src/tutorials/stack.rb +11 -0
- data/doc/src/tutorials/stack_01.page +224 -0
- data/doc/src/tutorials/stack_02.page +180 -0
- data/doc/src/tutorials/stack_03.page +291 -0
- data/doc/src/tutorials/stack_04.page +203 -0
- data/doc/src/tutorials/stack_04.page.orig +123 -0
- data/doc/src/tutorials/stack_05.page +90 -0
- data/doc/src/tutorials/stack_05.page.orig +124 -0
- data/doc/src/tutorials/stack_06.page +359 -0
- data/doc/src/tutorials/stack_06.page.orig +359 -0
- data/doc/src/tutorials/stack_spec.rb +41 -0
- data/examples/airport_spec.rb +4 -4
- data/examples/{spec_framework_spec.rb → bdd_framework_spec.rb} +6 -7
- data/examples/mocking_spec.rb +0 -5
- data/examples/stack_spec.rb +6 -7
- data/examples/sugar_spec.rb +14 -0
- data/lib/spec/api.rb +5 -2
- data/lib/spec/api/helper/should_base.rb +17 -22
- data/lib/spec/api/helper/should_helper.rb +4 -3
- data/lib/spec/api/helper/should_negator.rb +3 -2
- data/lib/spec/api/mocks/argument_expectation.rb +104 -0
- data/lib/spec/api/{mock.rb → mocks/message_expectation.rb} +47 -96
- data/lib/spec/api/mocks/mock.rb +63 -0
- data/lib/spec/api/mocks/order_group.rb +21 -0
- data/lib/spec/api/sugar.rb +47 -0
- data/lib/spec/rake/rcov_verify.rb +45 -0
- data/lib/spec/rake/spectask.rb +41 -56
- data/lib/spec/runner.rb +4 -1
- data/lib/spec/runner/backtrace_tweaker.rb +24 -3
- data/lib/spec/runner/base_text_formatter.rb +28 -0
- data/lib/spec/runner/context.rb +21 -18
- data/lib/spec/runner/context_runner.rb +20 -31
- data/lib/spec/runner/execution_context.rb +3 -3
- data/lib/spec/runner/kernel_ext.rb +10 -1
- data/lib/spec/runner/option_parser.rb +32 -14
- data/lib/spec/runner/progress_bar_formatter.rb +21 -0
- data/lib/spec/runner/rdoc_formatter.rb +15 -5
- data/lib/spec/runner/reporter.rb +100 -0
- data/lib/spec/runner/specdoc_formatter.rb +20 -0
- data/lib/spec/runner/specification.rb +42 -22
- data/lib/spec/version.rb +1 -1
- data/test/rcov/rcov_testtask.rb +1 -0
- data/test/spec/api/duck_type_test.rb +4 -4
- data/test/spec/api/helper/raising_test.rb +37 -17
- data/test/spec/api/{mock_arg_constraints_test.rb → mocks/mock_arg_constraints_test.rb} +10 -4
- data/test/spec/api/mocks/mock_ordering_test.rb +62 -0
- data/test/spec/api/{mock_test.rb → mocks/mock_test.rb} +30 -7
- data/test/spec/api/mocks/null_object_test.rb +31 -0
- data/test/spec/api/sugar_test.rb +71 -0
- data/test/spec/runner/backtrace_tweaker_test.rb +52 -4
- data/test/spec/runner/context_runner_test.rb +41 -21
- data/test/spec/runner/context_test.rb +60 -32
- data/test/spec/runner/execution_context_test.rb +4 -3
- data/test/spec/runner/failure_dump_test.rb +92 -0
- data/test/spec/runner/kernel_ext_test.rb +1 -2
- data/test/spec/runner/option_parser_test.rb +48 -28
- data/test/spec/runner/progress_bar_formatter_test.rb +48 -0
- data/test/spec/runner/rdoc_formatter_test.rb +31 -4
- data/test/spec/runner/reporter_test.rb +103 -0
- data/test/spec/runner/specdoc_formatter_test.rb +50 -0
- data/test/spec/runner/specification_test.rb +49 -11
- data/test/test_helper.rb +1 -4
- metadata +46 -15
- data/doc/src/community.page +0 -7
- data/doc/src/documentation/api.page +0 -185
- data/doc/src/why_rspec.page +0 -7
- data/examples/empty_stack_spec.rb +0 -22
- data/examples/team_spec.rb +0 -30
- data/lib/spec/api/duck_type.rb +0 -16
- data/lib/spec/runner/simple_text_reporter.rb +0 -88
- data/test/rcov/rcov_verify.rb +0 -28
- data/test/spec/runner/simple_text_reporter_test.rb +0 -123
@@ -0,0 +1,359 @@
|
|
1
|
+
h2. A Simple Stack - Pop - IN PROGRESS - DISREGARD THIS PAGE
|
2
|
+
|
3
|
+
So what next? We could specify a new context, or we could specify new messages in the existing contexts. How about 'pop'? We'll start with the empty stack:
|
4
|
+
|
5
|
+
<ruby>
|
6
|
+
context "An empty stack" do
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@stack = Stack.new
|
10
|
+
end
|
11
|
+
|
12
|
+
specify "should keep its mouth shut when you send it 'push'" do
|
13
|
+
lambda { @stack.push Object.new }.should.not.raise Exception
|
14
|
+
end
|
15
|
+
|
16
|
+
specify "should raise a StackUnderflowError when you send it 'top'" do
|
17
|
+
lambda { @stack.top }.should.raise StackUnderflowError
|
18
|
+
end
|
19
|
+
|
20
|
+
specify "should raise a StackUnderflowError when you send it 'pop'" do
|
21
|
+
lambda { @stack.pop }.should.raise StackUnderflowError
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
</ruby>
|
26
|
+
|
27
|
+
... run the specs ...
|
28
|
+
|
29
|
+
<pre>
|
30
|
+
$ spec stack_spec.rb -v
|
31
|
+
|
32
|
+
An empty stack
|
33
|
+
- should keep its mouth shut when you send it 'push'
|
34
|
+
- should raise a StackUnderflowError when you send it 'top'
|
35
|
+
- should raise a StackUnderflowError when you send it 'pop' (FAILED - 1)
|
36
|
+
|
37
|
+
A stack with one item
|
38
|
+
- should keep its mouth shut when you send it 'push'
|
39
|
+
- should return top when you send it 'top'
|
40
|
+
|
41
|
+
|
42
|
+
1)
|
43
|
+
ExpectationNotMetError in 'An empty stack should raise a StackUnderflowError when you send it 'pop''
|
44
|
+
<Proc> should raise <StackUnderflowError> but raised #<NoMethodError: undefined method `pop' for #<Stack:0x36f980>>
|
45
|
+
./stack_spec.rb:18:in `should raise a StackUnderflowError when you send it 'pop''
|
46
|
+
|
47
|
+
Finished in 0.000967 seconds
|
48
|
+
|
49
|
+
2 contexts, 5 specifications, 1 failure
|
50
|
+
</pre>
|
51
|
+
|
52
|
+
The messages here are slightly different than what we've seen so far. Back when we specified sending 'top' to an empty stack, we had already specified sending 'top' to a stack with one element, so we didn't get the <code>NoMethodError</code> that we get here. And since we were expecting a <code>StackUnderflowError</code>, the message tells us that we were expecting one sort of error but got another. This is excellent feedback, as it not only tells us that our expectation was not met, it also tells us exactly what went wrong and indirectly what to do about it: add the pop method:
|
53
|
+
|
54
|
+
<ruby>
|
55
|
+
class Stack
|
56
|
+
...
|
57
|
+
def pop
|
58
|
+
end
|
59
|
+
end
|
60
|
+
</ruby>
|
61
|
+
|
62
|
+
run the specs...
|
63
|
+
|
64
|
+
<pre>
|
65
|
+
$ spec stack_spec.rb -v
|
66
|
+
|
67
|
+
An empty stack
|
68
|
+
- should keep its mouth shut when you send it 'push'
|
69
|
+
- should raise a StackUnderflowError when you send it 'top'
|
70
|
+
- should raise a StackUnderflowError when you send it 'pop' (FAILED - 1)
|
71
|
+
|
72
|
+
A stack with one item
|
73
|
+
- should keep its mouth shut when you send it 'push'
|
74
|
+
- should return top when you send it 'top'
|
75
|
+
|
76
|
+
1)
|
77
|
+
ExpectationNotMetError in 'An empty stack should raise a StackUnderflowError when you send it 'pop''
|
78
|
+
<Proc> should raise <StackUnderflowError> but raised nothing
|
79
|
+
./stack_spec.rb:18:in `should raise a StackUnderflowError when you send it 'pop''
|
80
|
+
|
81
|
+
Finished in 0.000902 seconds
|
82
|
+
|
83
|
+
2 contexts, 5 specifications, 1 failure
|
84
|
+
</pre>
|
85
|
+
|
86
|
+
...and now the message tells us that nothing was raised. So back to stack.rb...
|
87
|
+
|
88
|
+
<ruby>
|
89
|
+
def pop
|
90
|
+
raise StackUnderflowError
|
91
|
+
end
|
92
|
+
</ruby>
|
93
|
+
|
94
|
+
...run the specs...
|
95
|
+
|
96
|
+
<pre>
|
97
|
+
$ spec stack_spec.rb -v
|
98
|
+
|
99
|
+
An empty stack
|
100
|
+
- should keep its mouth shut when you send it 'push'
|
101
|
+
- should raise a StackUnderflowError when you send it 'top'
|
102
|
+
- should raise a StackUnderflowError when you send it 'pop'
|
103
|
+
|
104
|
+
A stack with one item
|
105
|
+
- should keep its mouth shut when you send it 'push'
|
106
|
+
- should return top when you send it 'top'
|
107
|
+
|
108
|
+
Finished in 0.000812 seconds
|
109
|
+
|
110
|
+
2 contexts, 5 specifications, 0 failures
|
111
|
+
</pre>
|
112
|
+
|
113
|
+
...and they pass. Notice that we did not include any conditional logic in the pop method. As things stand right now, any client that calls pop on any stack (empty or otherwise) will get a StackUnderflowError. Obviously, this is not what we want, but again, rather than going back to the code we're going to specify the behaviour that we're looking for. We do have one other context in place already, so let's specify calling 'pop' on a context with one element.
|
114
|
+
|
115
|
+
<ruby>
|
116
|
+
context "A stack with one item" do
|
117
|
+
|
118
|
+
setup do
|
119
|
+
@stack = Stack.new
|
120
|
+
@stack.push "one item"
|
121
|
+
end
|
122
|
+
|
123
|
+
specify "should keep its mouth shut when you send it 'push'" do
|
124
|
+
lambda { @stack.push Object.new }.should.not.raise Exception
|
125
|
+
end
|
126
|
+
|
127
|
+
specify "should return top when you send it 'top'" do
|
128
|
+
@stack.top.should.equal "one item"
|
129
|
+
end
|
130
|
+
|
131
|
+
specify "should return top when you send it 'pop'" do
|
132
|
+
@stack.pop.should.equal "one item"
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
</ruby>
|
137
|
+
|
138
|
+
Run the specs...
|
139
|
+
|
140
|
+
<pre>
|
141
|
+
$ spec stack_spec.rb -v
|
142
|
+
|
143
|
+
An empty stack
|
144
|
+
- should keep its mouth shut when you send it 'push'
|
145
|
+
- should raise a StackUnderflowError when you send it 'top'
|
146
|
+
- should raise a StackUnderflowError when you send it 'pop'
|
147
|
+
|
148
|
+
A stack with one item
|
149
|
+
- should keep its mouth shut when you send it 'push'
|
150
|
+
- should return top when you send it 'top'
|
151
|
+
- should return top when you send it 'pop' (FAILED - 1)
|
152
|
+
|
153
|
+
1)
|
154
|
+
StackUnderflowError in 'A stack with one item should return top when you send it 'pop''
|
155
|
+
StackUnderflowError
|
156
|
+
./stack.rb:16:in `pop'
|
157
|
+
./stack_spec.rb:39:in `should return top when you send it 'pop''
|
158
|
+
|
159
|
+
Finished in 0.00102000000000002 seconds
|
160
|
+
|
161
|
+
2 contexts, 6 specifications, 1 failure
|
162
|
+
</pre>
|
163
|
+
|
164
|
+
...and we can see that we're getting the StackUnderflowError in a case where we don't want it. So NOW we can add the conditional logic to handle it...
|
165
|
+
|
166
|
+
<ruby>
|
167
|
+
class Stack
|
168
|
+
|
169
|
+
def push item
|
170
|
+
@item = item
|
171
|
+
end
|
172
|
+
|
173
|
+
def top
|
174
|
+
raise StackUnderflowError if @item.nil?
|
175
|
+
@item
|
176
|
+
end
|
177
|
+
|
178
|
+
def pop
|
179
|
+
raise StackUnderflowError if @item.nil?
|
180
|
+
@item
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
</ruby>
|
185
|
+
|
186
|
+
...run the specs...
|
187
|
+
|
188
|
+
<pre>
|
189
|
+
$ spec stack_spec.rb -v
|
190
|
+
|
191
|
+
An empty stack
|
192
|
+
- should keep its mouth shut when you send it 'push'
|
193
|
+
- should raise a StackUnderflowError when you send it 'top'
|
194
|
+
- should raise a StackUnderflowError when you send it 'pop'
|
195
|
+
|
196
|
+
A stack with one item
|
197
|
+
- should keep its mouth shut when you send it 'push'
|
198
|
+
- should return top when you send it 'top'
|
199
|
+
- should return top when you send it 'pop'
|
200
|
+
|
201
|
+
Finished in 0.000936 seconds
|
202
|
+
|
203
|
+
2 contexts, 6 specifications, 0 failures
|
204
|
+
</pre>
|
205
|
+
|
206
|
+
...and everything passes. Everything seems fine, but there's one thing missing. When you send 'pop' to a non-empty stack, it's also supposed to remove that element from the stack. Here's a typical solution to this problem using TDD:
|
207
|
+
|
208
|
+
<ruby>
|
209
|
+
#typical state-based example
|
210
|
+
def test_should_remove_top_item_when_sent_pop
|
211
|
+
assert_equal(stack.size, 1)
|
212
|
+
stack.pop
|
213
|
+
assert_equal(stack.size, 0)
|
214
|
+
end
|
215
|
+
</ruby>
|
216
|
+
|
217
|
+
That is VERY tempting because it seems so simple. It tells the story we want to tell. But it does so at the expense of exposing internal state. "So what?" you may ask. "We're talking about something trivial here." you may say. "Exposing size is perfectly logical because the stack is a collection" you may add. While all those things are reasonable, this thinking almost always takes us down the slippery slope of exposing more and more state just because it makes the test easier to write.
|
218
|
+
|
219
|
+
Now don't get me wrong. Tests should be easy to write. Testable code is a primary goal of what we're doing here. Whether you're doing TDD or BDD, that's absolutely key. But all to often we make bad design decisions in the name of testability, when there are perfectly reasonable alternatives right at our fingertips.
|
220
|
+
|
221
|
+
So even if you buy these arguments, TDD would probably lead you to this instead:
|
222
|
+
|
223
|
+
<ruby>
|
224
|
+
#typical state-based example
|
225
|
+
def test_should_remove_top_item_when_sent_pop
|
226
|
+
assert(!stack.empty?)
|
227
|
+
stack.pop
|
228
|
+
assert(stack.empty?)
|
229
|
+
end
|
230
|
+
</ruby>
|
231
|
+
|
232
|
+
That's a little better. We're exposing state, but it's not something as specific as size. But it's still state. We can do better.
|
233
|
+
|
234
|
+
What we're looking for here is observable behaviour. How does a one-element stack behave after we send it 'pop'. We've stated that the stack should be empty at that point, right? So perhaps the observable behaviour is that it acts like an empty stack:
|
235
|
+
|
236
|
+
<ruby>
|
237
|
+
specify "should raise a StackUnderflowError the second time you sent it 'pop'" do
|
238
|
+
@stack.pop
|
239
|
+
lambda { @stack.pop }.should.raise StackUnderflowError
|
240
|
+
end
|
241
|
+
</ruby>
|
242
|
+
|
243
|
+
Run the specs (this time without the -v flag)...
|
244
|
+
|
245
|
+
<pre>
|
246
|
+
$ spec stack_spec.rb
|
247
|
+
|
248
|
+
......F
|
249
|
+
|
250
|
+
1)
|
251
|
+
ExpectationNotMetError in 'A stack with one item should raise a StackUnderflowError the second time you sent it 'pop''
|
252
|
+
<Proc> should raise <StackUnderflowError> but raised nothing
|
253
|
+
./stack_spec.rb:44:in `should raise a StackUnderflowError the second time you sent it 'pop''
|
254
|
+
|
255
|
+
Finished in 0.000948 seconds
|
256
|
+
|
257
|
+
2 contexts, 7 specifications, 1 failure
|
258
|
+
</pre>
|
259
|
+
|
260
|
+
...implement just enough to meet this specification...
|
261
|
+
|
262
|
+
<ruby>
|
263
|
+
class Stack
|
264
|
+
|
265
|
+
...
|
266
|
+
|
267
|
+
def pop
|
268
|
+
raise StackUnderflowError if @item.nil?
|
269
|
+
item = @item
|
270
|
+
@item = nil
|
271
|
+
item
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
275
|
+
</ruby>
|
276
|
+
|
277
|
+
<pre>
|
278
|
+
$ spec stack_spec.rb
|
279
|
+
|
280
|
+
.......
|
281
|
+
|
282
|
+
Finished in 0.000859 seconds
|
283
|
+
|
284
|
+
2 contexts, 7 specifications, 0 failures
|
285
|
+
</pre>
|
286
|
+
|
287
|
+
...and all specifications are met. Let's look back at our one-item stack spec so far:
|
288
|
+
|
289
|
+
<ruby>
|
290
|
+
context "A stack with one item" do
|
291
|
+
|
292
|
+
setup do
|
293
|
+
@stack = Stack.new
|
294
|
+
@stack.push "one item"
|
295
|
+
end
|
296
|
+
|
297
|
+
specify "should keep its mouth shut when you send it 'push'" do
|
298
|
+
lambda { @stack.push Object.new }.should.not.raise Exception
|
299
|
+
end
|
300
|
+
|
301
|
+
specify "should return top when you send it 'top'" do
|
302
|
+
@stack.top.should.equal "one item"
|
303
|
+
end
|
304
|
+
|
305
|
+
specify "should return top when you send it 'pop'" do
|
306
|
+
@stack.pop.should.equal "one item"
|
307
|
+
end
|
308
|
+
|
309
|
+
specify "should raise a StackUnderflowError the second time you sent it 'pop'" do
|
310
|
+
@stack.pop
|
311
|
+
lambda { @stack.pop }.should.raise StackUnderflowError
|
312
|
+
end
|
313
|
+
|
314
|
+
end
|
315
|
+
</ruby>
|
316
|
+
|
317
|
+
See some imbalance? We've specified what happens when you send 'pop' repeatedly, but not what happens when you send 'top' repeatedly. 'top' should keep returning the same value, right?
|
318
|
+
|
319
|
+
<ruby>
|
320
|
+
context "A stack with one item" do
|
321
|
+
|
322
|
+
...
|
323
|
+
|
324
|
+
specify "should return top repeatedly when you send it 'top'" do
|
325
|
+
@stack.top.should.equal "one item"
|
326
|
+
@stack.top.should.equal "one item"
|
327
|
+
@stack.top.should.equal "one item"
|
328
|
+
end
|
329
|
+
|
330
|
+
...
|
331
|
+
|
332
|
+
end
|
333
|
+
</ruby>
|
334
|
+
|
335
|
+
Run the specs (with the -v flag)...
|
336
|
+
|
337
|
+
<pre>
|
338
|
+
$ spec stack_spec.rb -v
|
339
|
+
|
340
|
+
An empty stack
|
341
|
+
- should keep its mouth shut when you send it 'push'
|
342
|
+
- should raise a StackUnderflowError when you send it 'top'
|
343
|
+
- should raise a StackUnderflowError when you send it 'pop'
|
344
|
+
|
345
|
+
A stack with one item
|
346
|
+
- should keep its mouth shut when you send it 'push'
|
347
|
+
- should return top when you send it 'top'
|
348
|
+
- should return top repeatedly when you send it 'top'
|
349
|
+
- should return top when you send it 'pop'
|
350
|
+
- should raise a StackUnderflowError the second time you sent it 'pop'
|
351
|
+
|
352
|
+
Finished in 0.001746 seconds
|
353
|
+
|
354
|
+
2 contexts, 8 specifications, 0 failures
|
355
|
+
</pre>
|
356
|
+
|
357
|
+
So in this case we did not need any additional implementation, but look at how well rounded the specification becomes.
|
358
|
+
|
359
|
+
<a href="stack_04.html">Previous</a>
|
@@ -0,0 +1,359 @@
|
|
1
|
+
h2. A Simple Stack - Pop - IN PROGRESS - DISREGARD THIS PAGE
|
2
|
+
|
3
|
+
So what next? We could specify a new context, or we could specify new messages in the existing contexts. How about 'pop'? We'll start with the empty stack:
|
4
|
+
|
5
|
+
<ruby>
|
6
|
+
context "An empty stack" do
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@stack = Stack.new
|
10
|
+
end
|
11
|
+
|
12
|
+
specify "should keep its mouth shut when you send it 'push'" do
|
13
|
+
lambda { @stack.push Object.new }.should.not.raise Exception
|
14
|
+
end
|
15
|
+
|
16
|
+
specify "should raise a StackUnderflowError when you send it 'top'" do
|
17
|
+
lambda { @stack.top }.should.raise StackUnderflowError
|
18
|
+
end
|
19
|
+
|
20
|
+
specify "should raise a StackUnderflowError when you send it 'pop'" do
|
21
|
+
lambda { @stack.pop }.should.raise StackUnderflowError
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
</ruby>
|
26
|
+
|
27
|
+
... run the specs ...
|
28
|
+
|
29
|
+
<pre>
|
30
|
+
$ spec stack_spec.rb -v
|
31
|
+
|
32
|
+
An empty stack
|
33
|
+
- should keep its mouth shut when you send it 'push'
|
34
|
+
- should raise a StackUnderflowError when you send it 'top'
|
35
|
+
- should raise a StackUnderflowError when you send it 'pop' (FAILED - 1)
|
36
|
+
|
37
|
+
A stack with one item
|
38
|
+
- should keep its mouth shut when you send it 'push'
|
39
|
+
- should return top when you send it 'top'
|
40
|
+
|
41
|
+
|
42
|
+
1)
|
43
|
+
ExpectationNotMetError in 'An empty stack should raise a StackUnderflowError when you send it 'pop''
|
44
|
+
<Proc> should raise <StackUnderflowError> but raised #<NoMethodError: undefined method `pop' for #<Stack:0x36f980>>
|
45
|
+
./stack_spec.rb:18:in `should raise a StackUnderflowError when you send it 'pop''
|
46
|
+
|
47
|
+
Finished in 0.000967 seconds
|
48
|
+
|
49
|
+
2 contexts, 5 specifications, 1 failure
|
50
|
+
</pre>
|
51
|
+
|
52
|
+
The messages here are slightly different than what we've seen so far. Back when we specified sending 'top' to an empty stack, we had already specified sending 'top' to a stack with one element, so we didn't get the <code>NoMethodError</code> that we get here. And since we were expecting a <code>StackUnderflowError</code>, the message tells us that we were expecting one sort of error but got another. This is excellent feedback, as it not only tells us that our expectation was not met, it also tells us exactly what went wrong and indirectly what to do about it: add the pop method:
|
53
|
+
|
54
|
+
<ruby>
|
55
|
+
class Stack
|
56
|
+
...
|
57
|
+
def pop
|
58
|
+
end
|
59
|
+
end
|
60
|
+
</ruby>
|
61
|
+
|
62
|
+
run the specs...
|
63
|
+
|
64
|
+
<pre>
|
65
|
+
$ spec stack_spec.rb -v
|
66
|
+
|
67
|
+
An empty stack
|
68
|
+
- should keep its mouth shut when you send it 'push'
|
69
|
+
- should raise a StackUnderflowError when you send it 'top'
|
70
|
+
- should raise a StackUnderflowError when you send it 'pop' (FAILED - 1)
|
71
|
+
|
72
|
+
A stack with one item
|
73
|
+
- should keep its mouth shut when you send it 'push'
|
74
|
+
- should return top when you send it 'top'
|
75
|
+
|
76
|
+
1)
|
77
|
+
ExpectationNotMetError in 'An empty stack should raise a StackUnderflowError when you send it 'pop''
|
78
|
+
<Proc> should raise <StackUnderflowError> but raised nothing
|
79
|
+
./stack_spec.rb:18:in `should raise a StackUnderflowError when you send it 'pop''
|
80
|
+
|
81
|
+
Finished in 0.000902 seconds
|
82
|
+
|
83
|
+
2 contexts, 5 specifications, 1 failure
|
84
|
+
</pre>
|
85
|
+
|
86
|
+
...and now the message tells us that nothing was raised. So back to stack.rb...
|
87
|
+
|
88
|
+
<ruby>
|
89
|
+
def pop
|
90
|
+
raise StackUnderflowError
|
91
|
+
end
|
92
|
+
</ruby>
|
93
|
+
|
94
|
+
...run the specs...
|
95
|
+
|
96
|
+
<pre>
|
97
|
+
$ spec stack_spec.rb -v
|
98
|
+
|
99
|
+
An empty stack
|
100
|
+
- should keep its mouth shut when you send it 'push'
|
101
|
+
- should raise a StackUnderflowError when you send it 'top'
|
102
|
+
- should raise a StackUnderflowError when you send it 'pop'
|
103
|
+
|
104
|
+
A stack with one item
|
105
|
+
- should keep its mouth shut when you send it 'push'
|
106
|
+
- should return top when you send it 'top'
|
107
|
+
|
108
|
+
Finished in 0.000812 seconds
|
109
|
+
|
110
|
+
2 contexts, 5 specifications, 0 failures
|
111
|
+
</pre>
|
112
|
+
|
113
|
+
...and they pass. Notice that we did not include any conditional logic in the pop method. As things stand right now, any client that calls pop on any stack (empty or otherwise) will get a StackUnderflowError. Obviously, this is not what we want, but again, rather than going back to the code we're going to specify the behaviour that we're looking for. We do have one other context in place already, so let's specify calling 'pop' on a context with one element.
|
114
|
+
|
115
|
+
<ruby>
|
116
|
+
context "A stack with one item" do
|
117
|
+
|
118
|
+
setup do
|
119
|
+
@stack = Stack.new
|
120
|
+
@stack.push "one item"
|
121
|
+
end
|
122
|
+
|
123
|
+
specify "should keep its mouth shut when you send it 'push'" do
|
124
|
+
lambda { @stack.push Object.new }.should.not.raise Exception
|
125
|
+
end
|
126
|
+
|
127
|
+
specify "should return top when you send it 'top'" do
|
128
|
+
@stack.top.should.equal "one item"
|
129
|
+
end
|
130
|
+
|
131
|
+
specify "should return top when you send it 'pop'" do
|
132
|
+
@stack.pop.should.equal "one item"
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
</ruby>
|
137
|
+
|
138
|
+
Run the specs...
|
139
|
+
|
140
|
+
<pre>
|
141
|
+
$ spec stack_spec.rb -v
|
142
|
+
|
143
|
+
An empty stack
|
144
|
+
- should keep its mouth shut when you send it 'push'
|
145
|
+
- should raise a StackUnderflowError when you send it 'top'
|
146
|
+
- should raise a StackUnderflowError when you send it 'pop'
|
147
|
+
|
148
|
+
A stack with one item
|
149
|
+
- should keep its mouth shut when you send it 'push'
|
150
|
+
- should return top when you send it 'top'
|
151
|
+
- should return top when you send it 'pop' (FAILED - 1)
|
152
|
+
|
153
|
+
1)
|
154
|
+
StackUnderflowError in 'A stack with one item should return top when you send it 'pop''
|
155
|
+
StackUnderflowError
|
156
|
+
./stack.rb:16:in `pop'
|
157
|
+
./stack_spec.rb:39:in `should return top when you send it 'pop''
|
158
|
+
|
159
|
+
Finished in 0.00102000000000002 seconds
|
160
|
+
|
161
|
+
2 contexts, 6 specifications, 1 failure
|
162
|
+
</pre>
|
163
|
+
|
164
|
+
...and we can see that we're getting the StackUnderflowError in a case where we don't want it. So NOW we can add the conditional logic to handle it...
|
165
|
+
|
166
|
+
<ruby>
|
167
|
+
class Stack
|
168
|
+
|
169
|
+
def push item
|
170
|
+
@item = item
|
171
|
+
end
|
172
|
+
|
173
|
+
def top
|
174
|
+
raise StackUnderflowError if @item.nil?
|
175
|
+
@item
|
176
|
+
end
|
177
|
+
|
178
|
+
def pop
|
179
|
+
raise StackUnderflowError if @item.nil?
|
180
|
+
@item
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
</ruby>
|
185
|
+
|
186
|
+
...run the specs...
|
187
|
+
|
188
|
+
<pre>
|
189
|
+
$ spec stack_spec.rb -v
|
190
|
+
|
191
|
+
An empty stack
|
192
|
+
- should keep its mouth shut when you send it 'push'
|
193
|
+
- should raise a StackUnderflowError when you send it 'top'
|
194
|
+
- should raise a StackUnderflowError when you send it 'pop'
|
195
|
+
|
196
|
+
A stack with one item
|
197
|
+
- should keep its mouth shut when you send it 'push'
|
198
|
+
- should return top when you send it 'top'
|
199
|
+
- should return top when you send it 'pop'
|
200
|
+
|
201
|
+
Finished in 0.000936 seconds
|
202
|
+
|
203
|
+
2 contexts, 6 specifications, 0 failures
|
204
|
+
</pre>
|
205
|
+
|
206
|
+
...and everything passes. Everything seems fine, but there's one thing missing. When you send 'pop' to a non-empty stack, it's also supposed to remove that element from the stack. Here's a typical solution to this problem using TDD:
|
207
|
+
|
208
|
+
<ruby>
|
209
|
+
#typical state-based example
|
210
|
+
def test_should_remove_top_item_when_sent_pop
|
211
|
+
assert_equal(stack.size, 1)
|
212
|
+
stack.pop
|
213
|
+
assert_equal(stack.size, 0)
|
214
|
+
end
|
215
|
+
</ruby>
|
216
|
+
|
217
|
+
That is VERY tempting because it seems so simple. It tells the story we want to tell. But it does so at the expense of exposing internal state. "So what?" you may ask. "We're talking about something trivial here." you may say. "Exposing size is perfectly logical because the stack is a collection" you may add. While all those things are reasonable, this thinking almost always takes us down the slippery slope of exposing more and more state just because it makes the test easier to write.
|
218
|
+
|
219
|
+
Now don't get me wrong. Tests should be easy to write. Testable code is a primary goal of what we're doing here. Whether you're doing TDD or BDD, that's absolutely key. But all to often we make bad design decisions in the name of testability, when there are perfectly reasonable alternatives right at our fingertips.
|
220
|
+
|
221
|
+
So even if you buy these arguments, TDD would probably lead you to this instead:
|
222
|
+
|
223
|
+
<ruby>
|
224
|
+
#typical state-based example
|
225
|
+
def test_should_remove_top_item_when_sent_pop
|
226
|
+
assert(!stack.empty?)
|
227
|
+
stack.pop
|
228
|
+
assert(stack.empty?)
|
229
|
+
end
|
230
|
+
</ruby>
|
231
|
+
|
232
|
+
That's a little better. We're exposing state, but it's not something as specific as size. But it's still state. We can do better.
|
233
|
+
|
234
|
+
What we're looking for here is observable behaviour. How does a one-element stack behave after we send it 'pop'. We've stated that the stack should be empty at that point, right? So perhaps the observable behaviour is that it acts like an empty stack:
|
235
|
+
|
236
|
+
<ruby>
|
237
|
+
specify "should raise a StackUnderflowError the second time you sent it 'pop'" do
|
238
|
+
@stack.pop
|
239
|
+
lambda { @stack.pop }.should.raise StackUnderflowError
|
240
|
+
end
|
241
|
+
</ruby>
|
242
|
+
|
243
|
+
Run the specs (this time without the -v flag)...
|
244
|
+
|
245
|
+
<pre>
|
246
|
+
$ spec stack_spec.rb
|
247
|
+
|
248
|
+
......F
|
249
|
+
|
250
|
+
1)
|
251
|
+
ExpectationNotMetError in 'A stack with one item should raise a StackUnderflowError the second time you sent it 'pop''
|
252
|
+
<Proc> should raise <StackUnderflowError> but raised nothing
|
253
|
+
./stack_spec.rb:44:in `should raise a StackUnderflowError the second time you sent it 'pop''
|
254
|
+
|
255
|
+
Finished in 0.000948 seconds
|
256
|
+
|
257
|
+
2 contexts, 7 specifications, 1 failure
|
258
|
+
</pre>
|
259
|
+
|
260
|
+
...implement just enough to meet this specification...
|
261
|
+
|
262
|
+
<ruby>
|
263
|
+
class Stack
|
264
|
+
|
265
|
+
...
|
266
|
+
|
267
|
+
def pop
|
268
|
+
raise StackUnderflowError if @item.nil?
|
269
|
+
item = @item
|
270
|
+
@item = nil
|
271
|
+
item
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
275
|
+
</ruby>
|
276
|
+
|
277
|
+
<pre>
|
278
|
+
$ spec stack_spec.rb
|
279
|
+
|
280
|
+
.......
|
281
|
+
|
282
|
+
Finished in 0.000859 seconds
|
283
|
+
|
284
|
+
2 contexts, 7 specifications, 0 failures
|
285
|
+
</pre>
|
286
|
+
|
287
|
+
...and all specifications are met. Let's look back at our one-item stack spec so far:
|
288
|
+
|
289
|
+
<ruby>
|
290
|
+
context "A stack with one item" do
|
291
|
+
|
292
|
+
setup do
|
293
|
+
@stack = Stack.new
|
294
|
+
@stack.push "one item"
|
295
|
+
end
|
296
|
+
|
297
|
+
specify "should keep its mouth shut when you send it 'push'" do
|
298
|
+
lambda { @stack.push Object.new }.should.not.raise Exception
|
299
|
+
end
|
300
|
+
|
301
|
+
specify "should return top when you send it 'top'" do
|
302
|
+
@stack.top.should.equal "one item"
|
303
|
+
end
|
304
|
+
|
305
|
+
specify "should return top when you send it 'pop'" do
|
306
|
+
@stack.pop.should.equal "one item"
|
307
|
+
end
|
308
|
+
|
309
|
+
specify "should raise a StackUnderflowError the second time you sent it 'pop'" do
|
310
|
+
@stack.pop
|
311
|
+
lambda { @stack.pop }.should.raise StackUnderflowError
|
312
|
+
end
|
313
|
+
|
314
|
+
end
|
315
|
+
</ruby>
|
316
|
+
|
317
|
+
See some imbalance? We've specified what happens when you send 'pop' repeatedly, but not what happens when you send 'top' repeatedly. 'top' should keep returning the same value, right?
|
318
|
+
|
319
|
+
<ruby>
|
320
|
+
context "A stack with one item" do
|
321
|
+
|
322
|
+
...
|
323
|
+
|
324
|
+
specify "should return top repeatedly when you send it 'top'" do
|
325
|
+
@stack.top.should.equal "one item"
|
326
|
+
@stack.top.should.equal "one item"
|
327
|
+
@stack.top.should.equal "one item"
|
328
|
+
end
|
329
|
+
|
330
|
+
...
|
331
|
+
|
332
|
+
end
|
333
|
+
</ruby>
|
334
|
+
|
335
|
+
Run the specs (with the -v flag)...
|
336
|
+
|
337
|
+
<pre>
|
338
|
+
$ spec stack_spec.rb -v
|
339
|
+
|
340
|
+
An empty stack
|
341
|
+
- should keep its mouth shut when you send it 'push'
|
342
|
+
- should raise a StackUnderflowError when you send it 'top'
|
343
|
+
- should raise a StackUnderflowError when you send it 'pop'
|
344
|
+
|
345
|
+
A stack with one item
|
346
|
+
- should keep its mouth shut when you send it 'push'
|
347
|
+
- should return top when you send it 'top'
|
348
|
+
- should return top repeatedly when you send it 'top'
|
349
|
+
- should return top when you send it 'pop'
|
350
|
+
- should raise a StackUnderflowError the second time you sent it 'pop'
|
351
|
+
|
352
|
+
Finished in 0.001746 seconds
|
353
|
+
|
354
|
+
2 contexts, 8 specifications, 0 failures
|
355
|
+
</pre>
|
356
|
+
|
357
|
+
So in this case we did not need any additional implementation, but look at how well rounded the specification becomes.
|
358
|
+
|
359
|
+
<a href="stack_04.html">Previous</a>
|