-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexample.py
More file actions
404 lines (304 loc) · 11.4 KB
/
example.py
File metadata and controls
404 lines (304 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
"""
Test-Driven Development — Example Code
========================================
Run this file:
python3 example.py
This is a complete TDD walkthrough — building a Stack class from scratch.
We'll go through every RED, GREEN, and REFACTOR phase so you can see TDD
in action.
A Stack is a "last in, first out" (LIFO) data structure. Think of a stack
of plates: you add to the top, and you take from the top.
Operations we want:
- push(item) — add an item to the top
- pop() — remove and return the top item
- peek() — look at the top item without removing it
- is_empty() — check if the stack has no items
- size() — how many items are on the stack
"""
import unittest
# =============================================================================
# THE FINAL IMPLEMENTATION
# =============================================================================
#
# This is what we end up with after all the TDD cycles below. In real TDD,
# you'd build this up gradually — but since this is one file, here's the
# finished product. The tests below tell the story of how we got here.
# =============================================================================
class Stack:
"""A simple stack data structure, built entirely through TDD."""
def __init__(self):
self._items = []
def is_empty(self):
return len(self._items) == 0
def push(self, item):
self._items.append(item)
def pop(self):
if self.is_empty():
raise IndexError("Cannot pop from an empty stack")
return self._items.pop()
def peek(self):
if self.is_empty():
raise IndexError("Cannot peek at an empty stack")
return self._items[-1]
def size(self):
return len(self._items)
# =============================================================================
# THE TDD JOURNEY — Each test class represents one Red-Green-Refactor cycle
# =============================================================================
#
# Read these in order. Each one shows:
# RED: The failing test we wrote first
# GREEN: The minimal code that made it pass
# REFACTOR: Any cleanup we did (if applicable)
#
# In practice, you'd write one test, run it, see it fail, write code, run it,
# see it pass, then clean up. Here we show the final passing state of each
# cycle with comments explaining the journey.
# =============================================================================
class TestCycle1_NewStackIsEmpty(unittest.TestCase):
"""
CYCLE 1: A new stack should be empty.
RED phase:
We write a test that creates a Stack and checks is_empty().
It fails because the Stack class doesn't exist yet!
>>> s = Stack()
>>> s.is_empty() # NameError: name 'Stack' is not defined
GREEN phase:
We create the Stack class with just enough code:
class Stack:
def is_empty(self):
return True
That's it! The simplest thing that could possibly work.
The test passes. We resist the urge to add anything else.
REFACTOR phase:
Nothing to clean up yet — the code is already minimal.
"""
def test_new_stack_is_empty(self):
# Arrange
s = Stack()
# Act & Assert
self.assertTrue(s.is_empty())
class TestCycle2_PushMakesStackNonEmpty(unittest.TestCase):
"""
CYCLE 2: After pushing an item, the stack should NOT be empty.
RED phase:
We write a test that pushes something and checks is_empty().
It fails because push() doesn't exist yet!
>>> s = Stack()
>>> s.push("hello") # AttributeError: 'Stack' has no attribute 'push'
GREEN phase:
We add push() and update is_empty():
class Stack:
def __init__(self):
self._items = []
def is_empty(self):
return len(self._items) == 0
def push(self, item):
self._items.append(item)
Now both tests pass. Notice we had to add __init__ with a list
to track items. That's fine — the test demanded storage.
REFACTOR phase:
We changed is_empty() from "return True" to checking the list.
Previous test still passes. Good.
"""
def test_push_makes_stack_non_empty(self):
# Arrange
s = Stack()
# Act
s.push("hello")
# Assert
self.assertFalse(s.is_empty())
class TestCycle3_PopReturnsLastPushedItem(unittest.TestCase):
"""
CYCLE 3: Popping should return the last item that was pushed.
RED phase:
>>> s = Stack()
>>> s.push("hello")
>>> s.pop() # AttributeError: 'Stack' has no attribute 'pop'
GREEN phase:
def pop(self):
return self._items.pop()
One line. That's all we need to make the test pass.
REFACTOR phase:
Nothing to clean up. Code is clean.
"""
def test_pop_returns_last_pushed_item(self):
# Arrange
s = Stack()
s.push("hello")
# Act
result = s.pop()
# Assert
self.assertEqual(result, "hello")
def test_pop_returns_items_in_lifo_order(self):
"""We add another test to verify LIFO ordering — still cycle 3."""
# Arrange
s = Stack()
s.push("first")
s.push("second")
s.push("third")
# Act & Assert — items come out in reverse order
self.assertEqual(s.pop(), "third")
self.assertEqual(s.pop(), "second")
self.assertEqual(s.pop(), "first")
class TestCycle4_PopFromEmptyStackRaisesError(unittest.TestCase):
"""
CYCLE 4: Popping from an empty stack should raise an error.
RED phase:
>>> s = Stack()
>>> s.pop() # This actually raises IndexError from list.pop()
# ...but we want a CLEAR error message.
We write a test that checks for IndexError with a helpful message.
It fails because our pop() just does self._items.pop() which gives
a generic "pop from empty list" message.
GREEN phase:
def pop(self):
if self.is_empty():
raise IndexError("Cannot pop from an empty stack")
return self._items.pop()
REFACTOR phase:
Nothing to clean up. The guard clause is clean and readable.
"""
def test_pop_from_empty_stack_raises_error(self):
s = Stack()
with self.assertRaises(IndexError) as context:
s.pop()
self.assertIn("empty stack", str(context.exception))
class TestCycle5_Peek(unittest.TestCase):
"""
CYCLE 5: Peeking should return the top item WITHOUT removing it.
RED phase:
>>> s = Stack()
>>> s.push("hello")
>>> s.peek() # AttributeError: 'Stack' has no attribute 'peek'
GREEN phase:
def peek(self):
if self.is_empty():
raise IndexError("Cannot peek at an empty stack")
return self._items[-1]
We added the empty check right away this time — we learned from
cycle 4 that we need to handle that case. (Some TDD purists would
say we should write the empty-peek test first, but being practical
is fine too.)
REFACTOR phase:
Nothing to clean up.
"""
def test_peek_returns_top_item(self):
# Arrange
s = Stack()
s.push("bottom")
s.push("top")
# Act
result = s.peek()
# Assert
self.assertEqual(result, "top")
def test_peek_does_not_remove_item(self):
"""This is the key difference from pop — peek leaves the item."""
# Arrange
s = Stack()
s.push("hello")
# Act
s.peek()
# Assert — stack should NOT be empty after peek
self.assertFalse(s.is_empty())
self.assertEqual(s.size(), 1)
def test_peek_on_empty_stack_raises_error(self):
s = Stack()
with self.assertRaises(IndexError) as context:
s.peek()
self.assertIn("empty stack", str(context.exception))
class TestCycle6_Size(unittest.TestCase):
"""
CYCLE 6: The stack should report its size.
RED phase:
>>> s = Stack()
>>> s.size() # AttributeError: 'Stack' has no attribute 'size'
GREEN phase:
def size(self):
return len(self._items)
REFACTOR phase:
We look at the whole Stack class and realize... it's already clean!
Each method is small, clear, and does one thing. The TDD process
naturally led us to clean code.
Final version:
__init__ — creates empty list
is_empty — checks if list is empty
push — appends to list
pop — removes from end (with guard)
peek — reads from end (with guard)
size — returns length
Six methods, each one demanded by a test. No dead code, no
over-engineering. That's the power of TDD.
"""
def test_new_stack_has_size_zero(self):
s = Stack()
self.assertEqual(s.size(), 0)
def test_size_increases_with_push(self):
s = Stack()
s.push("a")
self.assertEqual(s.size(), 1)
s.push("b")
self.assertEqual(s.size(), 2)
s.push("c")
self.assertEqual(s.size(), 3)
def test_size_decreases_with_pop(self):
s = Stack()
s.push("a")
s.push("b")
s.pop()
self.assertEqual(s.size(), 1)
def test_push_and_pop_back_to_empty(self):
"""Full round trip — push items, pop them all, verify empty."""
s = Stack()
s.push("x")
s.push("y")
s.pop()
s.pop()
self.assertTrue(s.is_empty())
self.assertEqual(s.size(), 0)
# =============================================================================
# Summary
# =============================================================================
class TestStackIntegration(unittest.TestCase):
"""
After all 6 TDD cycles, we have a fully working Stack.
This integration test exercises the whole thing together — something you
might write at the end to verify the pieces work as a whole.
"""
def test_full_stack_workflow(self):
s = Stack()
# Start empty
self.assertTrue(s.is_empty())
self.assertEqual(s.size(), 0)
# Push some items
s.push(10)
s.push(20)
s.push(30)
self.assertEqual(s.size(), 3)
self.assertFalse(s.is_empty())
# Peek at the top
self.assertEqual(s.peek(), 30)
self.assertEqual(s.size(), 3) # peek doesn't remove
# Pop items in LIFO order
self.assertEqual(s.pop(), 30)
self.assertEqual(s.pop(), 20)
self.assertEqual(s.pop(), 10)
# Back to empty
self.assertTrue(s.is_empty())
# Popping again should raise
with self.assertRaises(IndexError):
s.pop()
# =============================================================================
# Run it!
# =============================================================================
if __name__ == "__main__":
print("=" * 60)
print("TDD Example: Building a Stack Class")
print("=" * 60)
print()
print("Each test class below represents one TDD cycle.")
print("Read the docstrings to follow the RED-GREEN-REFACTOR journey.")
print()
print("-" * 60)
# Run with some verbosity so you can see each test name
unittest.main(verbosity=2)