2
2
Working Incrementally
3
3
---------------------
4
4
5
- ((("Test-Driven Development (TDD)", "adapting existing code incrementally", id="TDDadapt07")))((("Testing Goat", "working state to working state"))) Now
6
- let's address our real problem, which is that our design only allows for
5
+ ((("Test-Driven Development (TDD)", "adapting existing code incrementally", id="TDDadapt07")))
6
+ ((("Testing Goat", "working state to working state")))
7
+ Now let's address our real problem, which is that our design only allows for
7
8
one global list. In this chapter I'll demonstrate a critical TDD technique:
8
9
how to adapt existing code using an incremental, step-by-step process which
9
10
takes you from working state to working state. Testing Goat, not Refactoring
10
11
Cat.
11
12
13
+
12
14
Small Design When Necessary
13
15
~~~~~~~~~~~~~~~~~~~~~~~~~~~
14
16
15
- ((("small vs. big design", id="small07")))((("multiple lists testing", "small vs. big design", id="MLTsmall07"))) Let's
16
- have a think about how we want support for multiple lists to
17
+ ((("small vs. big design", id="small07")))
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
17
20
work. Currently the FT (which is the closest we have to a design document)
18
21
says this:
19
22
@@ -38,23 +41,23 @@ don't see each other's lists, and each get their own URL as a way of
38
41
going back to their saved lists. What might a new design look like?
39
42
40
43
44
+
41
45
Not Big Design Up Front
42
46
^^^^^^^^^^^^^^^^^^^^^^^
43
47
44
-
45
-
46
- ((("agile movement")))((("Big Design Up Front "))) TDD
47
- is closely associated with the agile movement in software development,
48
+ ((("agile movement")))
49
+ ((("Big Design Up Front")))
50
+ ((("minimum viable applications ")))
51
+ TDD is closely associated with the agile movement in software development,
48
52
which includes a reaction against 'Big Design Up Front' , the
49
53
traditional software engineering practice whereby, after a lengthy requirements
50
54
gathering exercise, there is an equally lengthy design stage where the
51
55
software is planned out on paper. The agile philosophy is that you learn more
52
56
from solving problems in practice than in theory, especially when you confront
53
- your application with real users as soon as possible. ((("minimum viable applications"))) Instead
54
- of a long
55
- up-front design phase, we try to put a 'minimum viable application' out
56
- there early, and let the design evolve gradually based on feedback from
57
- real-world usage.
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.
58
61
59
62
60
63
But that doesn't mean that thinking about design is outright banned! In the
80
83
^^^^^^
81
84
82
85
83
- ((("Test-Driven Development (TDD)", "philosophy of", "YAGNI")))((("YAGNI (You ain’t gonna need it!)"))) Once
84
- you start thinking about design, it can be hard to stop. All sorts of
86
+ ((("Test-Driven Development (TDD)", "philosophy of", "YAGNI")))
87
+ ((("YAGNI (You ain’t gonna need it!)")))
88
+ Once you start thinking about design, it can be hard to stop. All sorts of
85
89
other thoughts are occurring to us-- we might want to give each list
86
90
a name or title, we might want to recognise users using usernames and
87
91
passwords, we might want to add a longer notes field as well as short
@@ -99,8 +103,9 @@ resist our overenthusiastic creative urges.
99
103
REST (ish)
100
104
^^^^^^^^^^
101
105
102
- ((("Representational State Transfer (REST)", "inspiration gained from")))((("Model-View-Controller (MVC) pattern"))) We
103
- have an idea of the data structure we want-- the Model part of
106
+ ((("Representational State Transfer (REST)", "inspiration gained from")))
107
+ ((("Model-View-Controller (MVC) pattern")))
108
+ We have an idea of the data structure we want-- the Model part of
104
109
Model-View-Controller (MVC). What about the View and Controller parts?
105
110
How should the user interact with ++List++s and their ++Item++ s using a web browser?
106
111
@@ -142,8 +147,9 @@ we can send POST requests:
142
147
use a PUT request here-- we're just using REST for inspiration. Apart from
143
148
anything else, you can't use PUT in a standard HTML form.)
144
149
145
- ((("", startref="small07")))((("", startref="MLTsmall07"))) In
146
- summary, our scratchpad for this chapter looks something like this:
150
+ ((("", startref="small07")))
151
+ ((("", startref="MLTsmall07")))
152
+ In summary, our scratchpad for this chapter looks something like this:
147
153
148
154
[role="scratchpad"]
149
155
*****
@@ -159,8 +165,9 @@ Implementing the New Design Incrementally Using TDD
159
165
160
166
161
167
162
- ((("Test-Driven Development (TDD)", "overall process of")))((("multiple lists testing", "incremental design implementation"))) How
163
- do we use TDD to implement the new design? Let's take another look at
168
+ ((("Test-Driven Development (TDD)", "overall process of")))
169
+ ((("multiple lists testing", "incremental design implementation")))
170
+ How do we use TDD to implement the new design? Let's take another look at
164
171
the flowchart for the TDD process in <<TDD-double-loop>>.
165
172
166
173
At the top level, we're going to use a combination of adding new functionality
@@ -184,8 +191,9 @@ image::images/twp2_0701.png["A flowchart showing functional tests as the overall
184
191
Ensuring We Have a Regression Test
185
192
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
186
193
187
- ((("regression", id="regression07")))((("multiple lists testing", "regression test", id="MLTregression07"))) Let's
188
- translate our scratchpad into a new functional test method, which
194
+ ((("regression", id="regression07")))
195
+ ((("multiple lists testing", "regression test", id="MLTregression07")))
196
+ Let's translate our scratchpad into a new functional test method, which
189
197
introduces a second user and checks that their to-do list is separate from
190
198
Edith's.
191
199
@@ -211,7 +219,7 @@ def test_can_start_a_list_for_one_user(self):
211
219
def test_multiple_users_can_start_lists_at_different_urls(self):
212
220
# Edith starts a new to-do list
213
221
self.browser.get(self.live_server_url)
214
- inputbox = self.browser.find_element_by_id( 'id_new_item')
222
+ inputbox = self.browser.find_element(By.ID, 'id_new_item')
215
223
inputbox.send_keys('Buy peacock feathers')
216
224
inputbox.send_keys(Keys.ENTER)
217
225
self.wait_for_row_in_list_table('1: Buy peacock feathers')
@@ -251,13 +259,13 @@ unique URL for their list:
251
259
# Francis visits the home page. There is no sign of Edith's
252
260
# list
253
261
self.browser.get(self.live_server_url)
254
- page_text = self.browser.find_element_by_tag_name( 'body').text
262
+ page_text = self.browser.find_element(By.TAG_NAME, 'body').text
255
263
self.assertNotIn('Buy peacock feathers', page_text)
256
264
self.assertNotIn('make a fly', page_text)
257
265
258
266
# Francis starts a new list by entering a new item. He
259
267
# is less interesting than Edith...
260
- inputbox = self.browser.find_element_by_id( 'id_new_item')
268
+ inputbox = self.browser.find_element(By.ID, 'id_new_item')
261
269
inputbox.send_keys('Buy milk')
262
270
inputbox.send_keys(Keys.ENTER)
263
271
self.wait_for_row_in_list_table('1: Buy milk')
@@ -268,7 +276,7 @@ unique URL for their list:
268
276
self.assertNotEqual(francis_list_url, edith_list_url)
269
277
270
278
# Again, there is no trace of Edith's list
271
- page_text = self.browser.find_element_by_tag_name( 'body').text
279
+ page_text = self.browser.find_element(By.TAG_NAME, 'body').text
272
280
self.assertNotIn('Buy peacock feathers', page_text)
273
281
self.assertIn('Buy milk', page_text)
274
282
@@ -295,11 +303,12 @@ $ pass:quotes[*python manage.py test functional_tests*]
295
303
[...]
296
304
.F
297
305
======================================================================
298
- FAIL: test_multiple_users_can_start_lists_at_different_urls
299
- (functional_tests.tests.NewVisitorTest)
306
+ FAIL: test_multiple_users_can_start_lists_at_different_urls (functional_tests.t
307
+ ests.NewVisitorTest.test_multiple_users_can_start_lists_at_different_urls)
308
+
300
309
---------------------------------------------------------------------
301
310
Traceback (most recent call last):
302
- File "...python-tdd-book/functional_tests/tests.py", line 83 , in
311
+ File "...python-tdd-book/functional_tests/tests.py", line 84 , in
303
312
test_multiple_users_can_start_lists_at_different_urls
304
313
self.assertRegex(edith_list_url, '/lists/.+')
305
314
AssertionError: Regex didn't match: '/lists/.+' not found in
@@ -399,15 +408,15 @@ the list table; it's because the URL
399
408
[keep-together]#'/the-only-list-in-the-world/'# doesn't exist yet!
400
409
401
410
----
402
- File "...python-tdd-book/functional_tests/tests.py", line 57 , in
411
+ File "...python-tdd-book/functional_tests/tests.py", line 58 , in
403
412
test_can_start_a_list_for_one_user
404
413
[...]
405
414
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate
406
415
element: [id="id_list_table"]
407
416
408
417
[...]
409
418
410
- File "...python-tdd-book/functional_tests/tests.py", line 79 , in
419
+ File "...python-tdd-book/functional_tests/tests.py", line 80 , in
411
420
test_multiple_users_can_start_lists_at_different_urls
412
421
self.wait_for_row_in_list_table('1: Buy peacock feathers')
413
422
[...]
@@ -479,7 +488,7 @@ TIP: ((("troubleshooting", "URL mappings")))Watch
479
488
out for trailing slashes in URLs, both here in the tests and in
480
489
'urls.py'. They're a common source of bugs.
481
490
482
- //TODO: add or link to an explantion about leading and trailing slashes in
491
+ //TODO: add or link to an explanation about leading and trailing slashes in
483
492
//urlpatterns, redirects, etc.
484
493
485
494
@@ -489,8 +498,8 @@ out for trailing slashes in URLs, both here in the tests and in
489
498
[source,python]
490
499
----
491
500
urlpatterns = [
492
- url(r'^$ ', views.home_page, name='home'),
493
- url(r'^ lists/the-only-list-in-the-world/$ ', views.view_list, name='view_list'),
501
+ path(' ', views.home_page, name='home'),
502
+ path(' lists/the-only-list-in-the-world/', views.view_list, name='view_list'),
494
503
]
495
504
----
496
505
====
@@ -500,15 +509,20 @@ urlpatterns = [
500
509
Running the tests again, we get:
501
510
502
511
----
512
+ [...]
513
+ AttributeError: module 'lists.views' has no attribute 'view_list'
514
+ File "...python-tdd-book/superlists/urls.py", line 6, in <module>
515
+ path('lists/the-only-list-in-the-world/', views.view_list,
516
+ name='view_list'),
517
+ ^^^^^^^^^^^^^^^
503
518
AttributeError: module 'lists.views' has no attribute 'view_list'
504
519
----
505
520
506
521
522
+
507
523
A New View Function
508
524
^^^^^^^^^^^^^^^^^^^
509
525
510
-
511
-
512
526
Nicely self-explanatory. Let's create a dummy view function in
513
527
'lists/views.py':
514
528
@@ -559,13 +573,14 @@ Now let's try the FTs again and see what they tell us:
559
573
----
560
574
FAIL: test_can_start_a_list_for_one_user
561
575
[...]
562
- File "...python-tdd-book/functional_tests/tests.py", line 67 , in
576
+ File "...python-tdd-book/functional_tests/tests.py", line 68 , in
563
577
test_can_start_a_list_for_one_user
564
578
[...]
565
579
AssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Buy
566
580
peacock feathers']
567
581
568
- FAIL: test_multiple_users_can_start_lists_at_different_urls
582
+ FAIL: test_multiple_users_can_start_lists_at_different_urls (functional_tests.t
583
+ ests.NewVisitorTest.test_multiple_users_can_start_lists_at_different_urls)
569
584
[...]
570
585
AssertionError: 'Buy peacock feathers' unexpectedly found in 'Your To-Do
571
586
list\n1: Buy peacock feathers'
@@ -625,7 +640,8 @@ Try that, and we'll see our FTs get back to a happier place:
625
640
626
641
[subs="specialcharacters,macros"]
627
642
----
628
- FAIL: test_multiple_users_can_start_lists_at_different_urls
643
+ FAIL: test_multiple_users_can_start_lists_at_different_urls (functional_tests.t
644
+ ests.NewVisitorTest.test_multiple_users_can_start_lists_at_different_urls)
629
645
[...]
630
646
AssertionError: 'Buy peacock feathers' unexpectedly found in 'Your To-Do
631
647
list\n1: Buy peacock feathers'
@@ -953,9 +969,9 @@ Let's build our new URL now:
953
969
[source,python]
954
970
----
955
971
urlpatterns = [
956
- url(r'^$ ', views.home_page, name='home'),
957
- url(r'^ lists/new$ ', views.new_list, name='new_list'),
958
- url(r'^ lists/the-only-list-in-the-world/$ ', views.view_list, name='view_list'),
972
+ path(' ', views.home_page, name='home'),
973
+ path(' lists/new', views.new_list, name='new_list'),
974
+ path(' lists/the-only-list-in-the-world/', views.view_list, name='view_list'),
959
975
]
960
976
----
961
977
====
@@ -1080,11 +1096,12 @@ test_can_start_a_list_for_one_user
1080
1096
self.wait_for_row_in_list_table('1: Buy peacock feathers')
1081
1097
File "...python-tdd-book/functional_tests/tests.py", line 23, in
1082
1098
wait_for_row_in_list_table
1083
- table = self.browser.find_element_by_id( 'id_list_table')
1099
+ table = self.browser.find_element(By.ID, 'id_list_table')
1084
1100
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate
1085
1101
element: [id="id_list_table"]
1086
1102
1087
- ERROR: test_multiple_users_can_start_lists_at_different_urls
1103
+ FAIL: test_multiple_users_can_start_lists_at_different_urls (functional_tests.t
1104
+ ests.NewVisitorTest.test_multiple_users_can_start_lists_at_different_urls)
1088
1105
[...]
1089
1106
File "...python-tdd-book/functional_tests/tests.py", line 79, in
1090
1107
test_multiple_users_can_start_lists_at_different_urls
@@ -1269,12 +1286,10 @@ AttributeError: 'Item' object has no attribute 'list'
1269
1286
----
1270
1287
1271
1288
1289
+
1272
1290
A Foreign Key Relationship
1273
1291
^^^^^^^^^^^^^^^^^^^^^^^^^^
1274
1292
1275
-
1276
-
1277
-
1278
1293
How do we give our `Item` a list attribute? Let's just try naively making it
1279
1294
like the `text` attribute (and here's your chance to see whether your
1280
1295
solution so far looks like mine by the way):
@@ -1337,7 +1352,7 @@ class List(models.Model):
1337
1352
1338
1353
class Item(models.Model):
1339
1354
text = models.TextField(default='')
1340
- list = models.ForeignKey(List, default=None)
1355
+ list = models.ForeignKey(List, default=None, on_delete=models.CASCADE )
1341
1356
----
1342
1357
====
1343
1358
//030-2
@@ -1579,17 +1594,18 @@ It's time to learn how we can pass parameters from URLs to views:
1579
1594
[source,python]
1580
1595
----
1581
1596
urlpatterns = [
1582
- url(r'^$ ', views.home_page, name='home'),
1583
- url(r'^ lists/new$ ', views.new_list, name='new_list'),
1584
- url(r'^ lists/(.+)/$ ', views.view_list, name='view_list'),
1597
+ path(' ', views.home_page, name='home'),
1598
+ path(' lists/new', views.new_list, name='new_list'),
1599
+ path(' lists/<list_id>/ ', views.view_list, name='view_list'),
1585
1600
]
1586
1601
----
1587
1602
====
1588
1603
//34
1589
1604
1590
1605
1591
1606
We adjust the regular expression for our URL to include a 'capture group',
1592
- `(.+)`, which will match any characters, up to the following `/`. The captured
1607
+ `<int:list_id>`, which will match any characters, up to the following `/`,
1608
+ and try and convert them to being an integer. The captured
1593
1609
text will get passed to the view as an argument.
1594
1610
1595
1611
In other words, if we go to the URL '/lists/1/', `view_list` will get a second
@@ -1847,15 +1863,15 @@ a little strange. We haven't actually specified a URL for
1847
1863
getting a 301?
1848
1864
1849
1865
This was a bit of a puzzler! It's because we've used a very "greedy"
1850
- regular expression in our URL:
1866
+ capture group in our URL:
1851
1867
1852
1868
1853
1869
[role="sourcecode currentcontents"]
1854
1870
.superlists/urls.py
1855
1871
====
1856
1872
[source,python]
1857
1873
----
1858
- url(r'^ lists/(.+)/$ ', views.view_list, name='view_list'),
1874
+ path(' lists/<list_id>/ ', views.view_list, name='view_list'),
1859
1875
----
1860
1876
====
1861
1877
@@ -1867,14 +1883,14 @@ In this case, [keep-together]#'/lists/1/add_item/'# would be a match for
1867
1883
guesses that we actually wanted the URL with a trailing slash.
1868
1884
1869
1885
We can fix that by making our URL pattern explicitly capture only numerical
1870
- digits, by using the regular expression `\d `:
1886
+ digits, by using the prefix `int: `:
1871
1887
1872
1888
[role="sourcecode"]
1873
1889
.superlists/urls.py
1874
1890
====
1875
1891
[source,python]
1876
1892
----
1877
- url(r'^ lists/(\d+) /$', views.view_list, name='view_list'),
1893
+ path(' lists/<int:list_id> /$', views.view_list, name='view_list'),
1878
1894
----
1879
1895
====
1880
1896
//38
@@ -2198,13 +2214,12 @@ Then we replace three lines in 'superlists/urls.py' with an `include`:
2198
2214
====
2199
2215
[source,python]
2200
2216
----
2201
- from django.conf.urls import include, url
2202
- from lists import views as list_views #<1>
2203
- from lists import urls as list_urls #<1>
2217
+ from django.urls import path
2218
+ from lists import views as list_views
2204
2219
2205
2220
urlpatterns = [
2206
- url(r'^$ ', list_views .home_page, name='home'),
2207
- url(r'^ lists/', include(list_urls )), #<2>
2221
+ path(' ', views .home_page, name='home'),
2222
+ path(' lists/', include('lists.urls' )),
2208
2223
]
2209
2224
----
2210
2225
====
@@ -2215,7 +2230,7 @@ urlpatterns = [
2215
2230
let us import `views` and `urls` from multiple apps if we want--and indeed we
2216
2231
will need to later on in the book.
2217
2232
2218
- <2> Here's the `include`. Notice that it can take a part of a URL regex as a
2233
+ <2> Here's the `include`. Notice that it can take a part of a URL as a
2219
2234
prefix, which will be applied to all the included URLs (this is the bit
2220
2235
where we reduce duplication, as well as giving our code a better
2221
2236
structure).
0 commit comments