Skip to content

Commit 3001aa3

Browse files
committed
wip on working incrementally
1 parent 975e1a0 commit 3001aa3

File tree

2 files changed

+79
-64
lines changed

2 files changed

+79
-64
lines changed

chapter_working_incrementally.asciidoc

Lines changed: 78 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22
Working Incrementally
33
---------------------
44

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
78
one global list. In this chapter I'll demonstrate a critical TDD technique:
89
how to adapt existing code using an incremental, step-by-step process which
910
takes you from working state to working state. Testing Goat, not Refactoring
1011
Cat.
1112

13+
1214
Small Design When Necessary
1315
~~~~~~~~~~~~~~~~~~~~~~~~~~~
1416

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
1720
work. Currently the FT (which is the closest we have to a design document)
1821
says this:
1922

@@ -38,23 +41,23 @@ don't see each other's lists, and each get their own URL as a way of
3841
going back to their saved lists. What might a new design look like?
3942

4043

44+
4145
Not Big Design Up Front
4246
^^^^^^^^^^^^^^^^^^^^^^^
4347

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,
4852
which includes a reaction against 'Big Design Up Front', the
4953
traditional software engineering practice whereby, after a lengthy requirements
5054
gathering exercise, there is an equally lengthy design stage where the
5155
software is planned out on paper. The agile philosophy is that you learn more
5256
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.
5861

5962

6063
But that doesn't mean that thinking about design is outright banned! In the
@@ -80,8 +83,9 @@ YAGNI!
8083
^^^^^^
8184

8285

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
8589
other thoughts are occurring to us--we might want to give each list
8690
a name or title, we might want to recognise users using usernames and
8791
passwords, we might want to add a longer notes field as well as short
@@ -99,8 +103,9 @@ resist our overenthusiastic creative urges.
99103
REST (ish)
100104
^^^^^^^^^^
101105

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
104109
Model-View-Controller (MVC). What about the View and Controller parts?
105110
How should the user interact with ++List++s and their ++Item++s using a web browser?
106111

@@ -142,8 +147,9 @@ we can send POST requests:
142147
use a PUT request here--we're just using REST for inspiration. Apart from
143148
anything else, you can't use PUT in a standard HTML form.)
144149

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:
147153

148154
[role="scratchpad"]
149155
*****
@@ -159,8 +165,9 @@ Implementing the New Design Incrementally Using TDD
159165

160166

161167

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
164171
the flowchart for the TDD process in <<TDD-double-loop>>.
165172

166173
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
184191
Ensuring We Have a Regression Test
185192
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
186193

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
189197
introduces a second user and checks that their to-do list is separate from
190198
Edith's.
191199

@@ -211,7 +219,7 @@ def test_can_start_a_list_for_one_user(self):
211219
def test_multiple_users_can_start_lists_at_different_urls(self):
212220
# Edith starts a new to-do list
213221
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')
215223
inputbox.send_keys('Buy peacock feathers')
216224
inputbox.send_keys(Keys.ENTER)
217225
self.wait_for_row_in_list_table('1: Buy peacock feathers')
@@ -251,13 +259,13 @@ unique URL for their list:
251259
# Francis visits the home page. There is no sign of Edith's
252260
# list
253261
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
255263
self.assertNotIn('Buy peacock feathers', page_text)
256264
self.assertNotIn('make a fly', page_text)
257265
258266
# Francis starts a new list by entering a new item. He
259267
# 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')
261269
inputbox.send_keys('Buy milk')
262270
inputbox.send_keys(Keys.ENTER)
263271
self.wait_for_row_in_list_table('1: Buy milk')
@@ -268,7 +276,7 @@ unique URL for their list:
268276
self.assertNotEqual(francis_list_url, edith_list_url)
269277
270278
# 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
272280
self.assertNotIn('Buy peacock feathers', page_text)
273281
self.assertIn('Buy milk', page_text)
274282
@@ -295,11 +303,12 @@ $ pass:quotes[*python manage.py test functional_tests*]
295303
[...]
296304
.F
297305
======================================================================
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+
300309
---------------------------------------------------------------------
301310
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
303312
test_multiple_users_can_start_lists_at_different_urls
304313
self.assertRegex(edith_list_url, '/lists/.+')
305314
AssertionError: Regex didn't match: '/lists/.+' not found in
@@ -399,15 +408,15 @@ the list table; it's because the URL
399408
[keep-together]#'/the-only-list-in-the-world/'# doesn't exist yet!
400409

401410
----
402-
File "...python-tdd-book/functional_tests/tests.py", line 57, in
411+
File "...python-tdd-book/functional_tests/tests.py", line 58, in
403412
test_can_start_a_list_for_one_user
404413
[...]
405414
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate
406415
element: [id="id_list_table"]
407416
408417
[...]
409418
410-
File "...python-tdd-book/functional_tests/tests.py", line 79, in
419+
File "...python-tdd-book/functional_tests/tests.py", line 80, in
411420
test_multiple_users_can_start_lists_at_different_urls
412421
self.wait_for_row_in_list_table('1: Buy peacock feathers')
413422
[...]
@@ -479,7 +488,7 @@ TIP: ((("troubleshooting", "URL mappings")))Watch
479488
out for trailing slashes in URLs, both here in the tests and in
480489
'urls.py'. They're a common source of bugs.
481490

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
483492
//urlpatterns, redirects, etc.
484493

485494

@@ -489,8 +498,8 @@ out for trailing slashes in URLs, both here in the tests and in
489498
[source,python]
490499
----
491500
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'),
494503
]
495504
----
496505
====
@@ -500,15 +509,20 @@ urlpatterns = [
500509
Running the tests again, we get:
501510

502511
----
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+
^^^^^^^^^^^^^^^
503518
AttributeError: module 'lists.views' has no attribute 'view_list'
504519
----
505520

506521

522+
507523
A New View Function
508524
^^^^^^^^^^^^^^^^^^^
509525

510-
511-
512526
Nicely self-explanatory. Let's create a dummy view function in
513527
'lists/views.py':
514528

@@ -559,13 +573,14 @@ Now let's try the FTs again and see what they tell us:
559573
----
560574
FAIL: test_can_start_a_list_for_one_user
561575
[...]
562-
File "...python-tdd-book/functional_tests/tests.py", line 67, in
576+
File "...python-tdd-book/functional_tests/tests.py", line 68, in
563577
test_can_start_a_list_for_one_user
564578
[...]
565579
AssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Buy
566580
peacock feathers']
567581
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)
569584
[...]
570585
AssertionError: 'Buy peacock feathers' unexpectedly found in 'Your To-Do
571586
list\n1: Buy peacock feathers'
@@ -625,7 +640,8 @@ Try that, and we'll see our FTs get back to a happier place:
625640

626641
[subs="specialcharacters,macros"]
627642
----
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)
629645
[...]
630646
AssertionError: 'Buy peacock feathers' unexpectedly found in 'Your To-Do
631647
list\n1: Buy peacock feathers'
@@ -953,9 +969,9 @@ Let's build our new URL now:
953969
[source,python]
954970
----
955971
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'),
959975
]
960976
----
961977
====
@@ -1080,11 +1096,12 @@ test_can_start_a_list_for_one_user
10801096
self.wait_for_row_in_list_table('1: Buy peacock feathers')
10811097
File "...python-tdd-book/functional_tests/tests.py", line 23, in
10821098
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')
10841100
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate
10851101
element: [id="id_list_table"]
10861102
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)
10881105
[...]
10891106
File "...python-tdd-book/functional_tests/tests.py", line 79, in
10901107
test_multiple_users_can_start_lists_at_different_urls
@@ -1269,12 +1286,10 @@ AttributeError: 'Item' object has no attribute 'list'
12691286
----
12701287

12711288

1289+
12721290
A Foreign Key Relationship
12731291
^^^^^^^^^^^^^^^^^^^^^^^^^^
12741292

1275-
1276-
1277-
12781293
How do we give our `Item` a list attribute? Let's just try naively making it
12791294
like the `text` attribute (and here's your chance to see whether your
12801295
solution so far looks like mine by the way):
@@ -1337,7 +1352,7 @@ class List(models.Model):
13371352
13381353
class Item(models.Model):
13391354
text = models.TextField(default='')
1340-
list = models.ForeignKey(List, default=None)
1355+
list = models.ForeignKey(List, default=None, on_delete=models.CASCADE)
13411356
----
13421357
====
13431358
//030-2
@@ -1579,17 +1594,18 @@ It's time to learn how we can pass parameters from URLs to views:
15791594
[source,python]
15801595
----
15811596
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'),
15851600
]
15861601
----
15871602
====
15881603
//34
15891604

15901605

15911606
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
15931609
text will get passed to the view as an argument.
15941610

15951611
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
18471863
getting a 301?
18481864

18491865
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:
18511867

18521868

18531869
[role="sourcecode currentcontents"]
18541870
.superlists/urls.py
18551871
====
18561872
[source,python]
18571873
----
1858-
url(r'^lists/(.+)/$', views.view_list, name='view_list'),
1874+
path('lists/<list_id>/', views.view_list, name='view_list'),
18591875
----
18601876
====
18611877

@@ -1867,14 +1883,14 @@ In this case, [keep-together]#'/lists/1/add_item/'# would be a match for
18671883
guesses that we actually wanted the URL with a trailing slash.
18681884

18691885
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:`:
18711887

18721888
[role="sourcecode"]
18731889
.superlists/urls.py
18741890
====
18751891
[source,python]
18761892
----
1877-
url(r'^lists/(\d+)/$', views.view_list, name='view_list'),
1893+
path('lists/<int:list_id>/$', views.view_list, name='view_list'),
18781894
----
18791895
====
18801896
//38
@@ -2198,13 +2214,12 @@ Then we replace three lines in 'superlists/urls.py' with an `include`:
21982214
====
21992215
[source,python]
22002216
----
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
22042219
22052220
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')),
22082223
]
22092224
----
22102225
====
@@ -2215,7 +2230,7 @@ urlpatterns = [
22152230
let us import `views` and `urls` from multiple apps if we want--and indeed we
22162231
will need to later on in the book.
22172232

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
22192234
prefix, which will be applied to all the included URLs (this is the bit
22202235
where we reduce duplication, as well as giving our code a better
22212236
structure).

0 commit comments

Comments
 (0)