@@ -4,21 +4,20 @@ Working Incrementally
4
4
5
5
((("Test-Driven Development (TDD)", "adapting existing code incrementally", id="TDDadapt07")))
6
6
((("Testing Goat", "working state to working state")))
7
- Now let's address our real problem, which is that our design only allows for
8
- one global list. In this chapter I'll demonstrate a critical TDD technique:
9
- how to adapt existing code using an incremental, step-by-step process which
10
- takes you from working state to working state. Testing Goat, not Refactoring
11
- Cat.
7
+ Now let's address our real problem, which is that our design only allows for one global list.
8
+ In this chapter I'll demonstrate a critical TDD technique:
9
+ how to adapt existing code using an incremental, step-by-step process
10
+ which takes you from working state to working state.
11
+ Testing Goat, not Refactoring Cat.
12
12
13
13
14
14
Small Design When Necessary
15
15
~~~~~~~~~~~~~~~~~~~~~~~~~~~
16
16
17
17
((("small vs. big design", id="small07")))
18
18
((("multiple lists testing", "small vs. big design", id="MLTsmall07")))
19
- Let's have a think about how we want support for multiple lists to
20
- work. Currently the FT (which is the closest we have to a design document)
21
- says this:
19
+ Let's have a think about how we want support for multiple lists to work.
20
+ Currently the FT (which is the closest we have to a design document) says this:
22
21
23
22
[role="sourcecode currentcontents dofirst-ch07l000"]
24
23
.functional_tests/tests.py
@@ -36,9 +35,10 @@ says this:
36
35
----
37
36
====
38
37
39
- But really we want to expand on this, by saying that different users
40
- don't see each other's lists, and each get their own URL as a way of
41
- going back to their saved lists. What might a new design look like?
38
+ But really we want to expand on this,
39
+ by saying that different users don't see each other's lists,
40
+ and each get their own URL as a way of going back to their saved lists.
41
+ What might a new design look like?
42
42
43
43
44
44
@@ -49,34 +49,33 @@ Not Big Design Up Front
49
49
((("Big Design Up Front")))
50
50
((("minimum viable applications")))
51
51
TDD is closely associated with the agile movement in software development,
52
- which includes a reaction against 'Big Design Up Front' , the
53
- traditional software engineering practice whereby, after a lengthy requirements
54
- gathering exercise, there is an equally lengthy design stage where the
55
- software is planned out on paper. The agile philosophy is that you learn more
56
- from solving problems in practice than in theory, especially when you confront
57
- your application with real users as soon as possible.
58
- Instead of a long up-front design phase, we try to put a 'minimum viable
59
- application' out there early, and let the design evolve gradually based on
60
- feedback from real-world usage.
61
-
62
-
63
- But that doesn't mean that thinking about design is outright banned! In the
64
- last big chapter we saw how just blundering ahead without thinking can
65
- 'eventually' get us to the right answer, but often a little thinking about
66
- design can help us get there faster. So, let's think about our minimum viable
67
- lists app, and what kind of design we'll need to deliver it:
52
+ which includes a reaction against 'Big Design Up Front' ,
53
+ the traditional software engineering practice whereby,
54
+ after a lengthy requirements gathering exercise,
55
+ there is an equally lengthy design stage where the software is planned out on paper.
56
+ The agile philosophy is that you learn more from solving problems in practice than in theory,
57
+ especially when you confront your application with real users as soon as possible.
58
+ Instead of a long up-front design phase,
59
+ we try to put a 'minimum viable application' out there early,
60
+ and let the design evolve gradually based on feedback from real-world usage.
61
+
62
+
63
+ But that doesn't mean that thinking about design is outright banned!
64
+ In the last big chapter we saw how just blundering ahead without thinking can 'eventually' get us to the right answer,
65
+ but often a little thinking about design can help us get there faster.
66
+ So, let's think about our minimum viable lists app,
67
+ and what kind of design we'll need to deliver it:
68
68
69
69
* We want each user to be able to store their own list-- at least one, for now.
70
- * A list is made up of several items, whose primary attribute is a bit of
71
- descriptive text.
72
- * We need to save lists from one visit to the next. For now, we can give
73
- each user a unique URL for their list. Later on we may want some way of
74
- automatically recognising users and showing them their lists.
70
+ * A list is made up of several items, whose primary attribute is a bit of descriptive text.
71
+ * We need to save lists from one visit to the next.
72
+ For now, we can give each user a unique URL for their list.
73
+ Later on we may want some way of automatically recognising users and showing them their lists.
75
74
76
- To deliver the "for now" items, it sounds like we're going to store
77
- lists and their items in a database. Each list will have a unique URL,
78
- and each list item will be a bit of descriptive text, associated with a
79
- particular list.
75
+ To deliver the "for now" items,
76
+ it sounds like we're going to store lists and their items in a database.
77
+ Each list will have a unique URL,
78
+ and each list item will be a bit of descriptive text, associated with a particular list.
80
79
81
80
82
81
YAGNI!
@@ -85,19 +84,20 @@ YAGNI!
85
84
86
85
((("Test-Driven Development (TDD)", "philosophy of", "YAGNI")))
87
86
((("YAGNI (You ain’t gonna need it!)")))
88
- Once you start thinking about design, it can be hard to stop. All sorts of
89
- other thoughts are occurring to us-- we might want to give each list
90
- a name or title, we might want to recognise users using usernames and
91
- passwords, we might want to add a longer notes field as well as short
92
- descriptions to our list, we might want to store some kind of ordering, and so
93
- on. But we obey another tenet of the agile gospel: "YAGNI" (pronounced
94
- yag-knee), which stands for "You ain't gonna need it!" As software
95
- developers, we have fun creating things, and sometimes it's hard to resist
96
- the urge to build things just because an idea occurred to us and we 'might'
97
- need it. The trouble is that more often than not, no matter how cool the idea
98
- was, you 'won't' end up using it. Instead you have a load of unused code,
99
- adding to the complexity of your application. YAGNI is the mantra we use to
100
- resist our overenthusiastic creative urges.
87
+ Once you start thinking about design, it can be hard to stop.
88
+ All sorts of other thoughts are occurring to us-- we might want to give each list a name or title,
89
+ we might want to recognise users using usernames and passwords,
90
+ we might want to add a longer notes field as well as short descriptions to our list,
91
+ we might want to store some kind of ordering, and so on.
92
+ But we obey another tenet of the agile gospel: "YAGNI" (pronounced yag-knee),
93
+ which stands for "You ain't gonna need it!"
94
+ As software developers, we have fun creating things,
95
+ and sometimes it's hard to resist the urge to build things
96
+ just because an idea occurred to us and we 'might' need it.
97
+ The trouble is that more often than not, no matter how cool the idea was,
98
+ you 'won't' end up using it.
99
+ Instead you have a load of unused code, adding to the complexity of your application.
100
+ YAGNI is the mantra we use to resist our overenthusiastic creative urges.
101
101
102
102
103
103
REST (ish)
@@ -113,7 +113,7 @@ How should the user interact with ++List++s and their ++Item++s using a web brow
113
113
Representational State Transfer (REST) is an approach to web design that's
114
114
usually used to guide the design of web-based APIs. When designing a
115
115
user-facing site, it's not possible to stick 'strictly' to the REST rules,
116
- but they still provide some useful inspiration (skip ahead to
116
+ but they still provide some useful inspiration (skip ahead to
117
117
<<appendix_rest_api>> if you want to see a real REST API).
118
118
119
119
REST suggests that we have a URL structure that matches our data structure,
@@ -160,11 +160,10 @@ In summary, our scratchpad for this chapter looks something like this:
160
160
*****
161
161
162
162
163
+
163
164
Implementing the New Design Incrementally Using TDD
164
165
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
165
166
166
-
167
-
168
167
((("Test-Driven Development (TDD)", "overall process of")))
169
168
((("multiple lists testing", "incremental design implementation")))
170
169
How do we use TDD to implement the new design? Let's take another look at
@@ -175,7 +174,7 @@ At the top level, we're going to use a combination of adding new functionality
175
174
application-- that is, rewriting some of the existing implementation so that it
176
175
delivers the same functionality to the user but using aspects of our new
177
176
design. We'll be able to use the existing functional test to verify we don't
178
- break what already works, and the new functional test to drive the new
177
+ break what already works, and the new functional test to drive the new
179
178
features.
180
179
181
180
At the unit test level, we'll be adding new tests or modifying existing ones to
@@ -214,7 +213,7 @@ def test_can_start_a_list_for_one_user(self):
214
213
self.wait_for_row_in_list_table('1: Buy peacock feathers')
215
214
216
215
# Satisfied, she goes back to sleep
217
-
216
+
218
217
219
218
def test_multiple_users_can_start_lists_at_different_urls(self):
220
219
# Edith starts a new to-do list
@@ -291,8 +290,8 @@ using the convention of double-hashes (`##`) to indicate
291
290
User Story. They're a message to our future selves, which might otherwise
292
291
be wondering why the heck we're quitting the browser and starting a new
293
292
one...
294
-
295
-
293
+
294
+
296
295
297
296
Other than that, the new test is fairly self-explanatory. Let's see how we do
298
297
when we run our FTs:
@@ -661,7 +660,7 @@ Green? Refactor
661
660
662
661
663
662
((("multiple lists testing", "refactoring")))((("refactoring")))((("Red/Green/Refactor"))) Time
664
- for a little tidying up.
663
+ for a little tidying up.
665
664
666
665
In the 'Red/Green/Refactor' dance, we've arrived at green, so we should see
667
666
what needs a refactor. We now have two views, one for the home page, and one
@@ -685,7 +684,7 @@ class ItemModelTest(TestCase):
685
684
686
685
----
687
686
688
- We can definitely delete the `test_displays_all_list_items` method from
687
+ We can definitely delete the `test_displays_all_list_items` method from
689
688
`HomePageTest` ; it's no longer needed. If you run *`manage.py test lists`*
690
689
now, it should say it ran 6 tests instead of 7:
691
690
@@ -874,7 +873,7 @@ are we with our own to-do list?
874
873
875
874
We've 'sort of' made progress on the second item, even if there's still only
876
875
one list in the world. The first item is a bit scary. Can we do something
877
- about items 3 or 4?
876
+ about items 3 or 4?
878
877
879
878
Let's have a new URL for adding new list items. If nothing else, it'll
880
879
simplify the home page view.
@@ -942,7 +941,7 @@ code was 404 (expected 302)
942
941
----
943
942
944
943
The first failure tells us we're not saving a new item to the database, and the
945
- second says that, instead of returning a 302 redirect, our view is returning
944
+ second says that, instead of returning a 302 redirect, our view is returning
946
945
a 404. That's because we haven't built a URL for '/lists/new' , so the
947
946
`client.post` is just getting a "not found" response.
948
947
@@ -1091,19 +1090,22 @@ Oops:
1091
1090
----
1092
1091
ERROR: test_can_start_a_list_for_one_user
1093
1092
[...]
1094
- File "...python-tdd-book/functional_tests/tests.py", line 57 , in
1093
+ File "...python-tdd-book/functional_tests/tests.py", line 58 , in
1095
1094
test_can_start_a_list_for_one_user
1096
1095
self.wait_for_row_in_list_table('1: Buy peacock feathers')
1097
- File "...python-tdd-book/functional_tests/tests.py", line 23 , in
1096
+ File "...python-tdd-book/functional_tests/tests.py", line 24 , in
1098
1097
wait_for_row_in_list_table
1099
1098
table = self.browser.find_element(By.ID, 'id_list_table')
1099
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1100
+ [...]
1100
1101
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate
1101
1102
element: [id="id_list_table"]
1102
1103
1104
+
1103
1105
FAIL: test_multiple_users_can_start_lists_at_different_urls (functional_tests.t
1104
1106
ests.NewVisitorTest.test_multiple_users_can_start_lists_at_different_urls)
1105
1107
[...]
1106
- File "...python-tdd-book/functional_tests/tests.py", line 79 , in
1108
+ File "...python-tdd-book/functional_tests/tests.py", line 80 , in
1107
1109
test_multiple_users_can_start_lists_at_different_urls
1108
1110
self.wait_for_row_in_list_table('1: Buy peacock feathers')
1109
1111
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate
@@ -1184,16 +1186,16 @@ present the changes in the form of a diff:
1184
1186
from django.test import TestCase
1185
1187
-from lists.models import Item
1186
1188
+from lists.models import Item, List
1187
-
1188
-
1189
+
1190
+
1189
1191
class HomePageTest(TestCase):
1190
1192
@@ -44,22 +44,32 @@ class ListViewTest(TestCase):
1191
-
1192
-
1193
-
1193
+
1194
+
1195
+
1194
1196
-class ItemModelTest(TestCase):
1195
1197
+class ListAndItemModelsTest(TestCase):
1196
-
1198
+
1197
1199
def test_saving_and_retrieving_items(self):
1198
1200
+ list_ = List()
1199
1201
+ list_.save()
@@ -1202,18 +1204,18 @@ present the changes in the form of a diff:
1202
1204
first_item.text = 'The first (ever) list item'
1203
1205
+ first_item.list = list_
1204
1206
first_item.save()
1205
-
1207
+
1206
1208
second_item = Item()
1207
1209
second_item.text = 'Item the second'
1208
1210
+ second_item.list = list_
1209
1211
second_item.save()
1210
-
1212
+
1211
1213
+ saved_list = List.objects.first()
1212
1214
+ self.assertEqual(saved_list, list_)
1213
1215
+
1214
1216
saved_items = Item.objects.all()
1215
1217
self.assertEqual(saved_items.count(), 2)
1216
-
1218
+
1217
1219
first_saved_item = saved_items[0]
1218
1220
second_saved_item = saved_items[1]
1219
1221
self.assertEqual(first_saved_item.text, 'The first (ever) list item')
@@ -1236,7 +1238,7 @@ NOTE: I'm using the variable name `list_` to avoid "shadowing" the Python
1236
1238
built-in `list` function. It's ugly, but all the other options I tried
1237
1239
were equally ugly or worse (`my_list` , `the_list` , `list1` , `listey` ... ).
1238
1240
1239
- Time for another unit-test/code cycle.
1241
+ Time for another unit-test/code cycle.
1240
1242
1241
1243
For the first couple of iterations, rather than explicitly showing you what
1242
1244
code to enter in between every test run, I'm only going to show you the
@@ -1379,7 +1381,7 @@ WARNING: Deleting migrations is dangerous. We do need to do it now and again,
1379
1381
Django will be confused about what state it's in, and how to apply future
1380
1382
migrations. You should only do it when you're sure the migration hasn't
1381
1383
been used. A good rule of thumb is that you should never delete or modify
1382
- a migration that's already been committed to your VCS.
1384
+ a migration that's already been committed to your VCS.
1383
1385
1384
1386
1385
1387
@@ -1406,13 +1408,13 @@ Ran 6 tests in 0.021s
1406
1408
FAILED (errors=3)
1407
1409
----
1408
1410
1409
- Oh dear!
1411
+ Oh dear!
1410
1412
1411
1413
There is some good news. Although it's hard to see, our model tests are
1412
1414
passing. But three of our view tests are failing nastily.
1413
1415
1414
1416
The reason is because of the new relationship we've introduced between
1415
- ++Item++s and ++List++s, which requires each item to have a parent list, which
1417
+ ++Item++s and ++List++ s, which requires each item to have a parent list, which
1416
1418
our old tests and code aren't prepared for.
1417
1419
1418
1420
Still, this is exactly why we have tests! Let's get them working again. The
@@ -1563,11 +1565,11 @@ class ListViewTest(TestCase):
1563
1565
1564
1566
1565
1567
NOTE: A couple more of those lovely f-strings in this listing! If they're
1566
- still a bit of a mystery, take a look at the
1568
+ still a bit of a mystery, take a look at the
1567
1569
https://docs.python.org/3/reference/lexical_analysis.html#f-strings[docs]
1568
1570
(although if your formal CS education is as bad as mine, you'll probably
1569
1571
skip the formal grammar).
1570
-
1572
+
1571
1573
1572
1574
Running the unit tests gives an expected 404, and another related error:
1573
1575
@@ -1760,7 +1762,7 @@ FAIL: test_can_start_a_list_for_one_user
1760
1762
(functional_tests.tests.NewVisitorTest)
1761
1763
---------------------------------------------------------------------
1762
1764
Traceback (most recent call last):
1763
- File "...python-tdd-book/functional_tests/tests.py", line 67 , in
1765
+ File "...python-tdd-book/functional_tests/tests.py", line 68 , in
1764
1766
test_can_start_a_list_for_one_user
1765
1767
self.wait_for_row_in_list_table('2: Use peacock feathers to make a fly')
1766
1768
[...]
@@ -2139,7 +2141,7 @@ But we can fix it in 'list.html', as well as adjusting the form's POST action:
2139
2141
<1> There's our new form action.
2140
2142
2141
2143
<2> `.item_set` is ((("reverse lookups"))) called
2142
- a
2144
+ a
2143
2145
https://docs.djangoproject.com/en/1.11/topics/db/queries/#following-relationships-backward[reverse lookup].
2144
2146
It's one of Django's incredibly useful bits of ORM that lets you look up an
2145
2147
object's related items from a different table...
@@ -2256,7 +2258,7 @@ urlpatterns = [
2256
2258
----
2257
2259
====
2258
2260
2259
- Rerun the unit tests to check that everything worked.
2261
+ Rerun the unit tests to check that everything worked.
2260
2262
2261
2263
2262
2264
When I did it, I couldn't quite believe I did it correctly on the first go. It
@@ -2298,9 +2300,9 @@ natural urge is often to dive in and fix everything at once...but if
2298
2300
loads of changes to our code and nothing working. The Testing Goat
2299
2301
encourages us to take one step at a time, and go from working state to
2300
2302
working state.
2301
-
2302
-
2303
-
2303
+
2304
+
2305
+
2304
2306
2305
2307
2306
2308
Split work out into small, achievable tasks::
@@ -2309,11 +2311,11 @@ this means starting with "boring" work rather than diving
2309
2311
straight in with the fun stuff, but you'll have to trust that YOLO-you
2310
2312
in the parallel universe is probably having a bad time, having broken
2311
2313
everything, and struggling to get the app working again.
2312
-
2314
+
2313
2315
2314
2316
YAGNI::
2315
2317
((("Test-Driven Development (TDD)", "philosophy of", "YAGNI")))((("YAGNI (You ain’t gonna need it!)")))You
2316
- ain't gonna need it! Avoid the temptation to write code that you
2318
+ ain't gonna need it! Avoid the temptation to write code that you
2317
2319
think 'might' be useful, just because it suggests itself at the time.
2318
2320
Chances are, you won't use it, or you won't have anticipated your
2319
2321
future requirements correctly. See <<chapter_outside_in>> for one
0 commit comments