forked from hjwp/Test-Driven-Django-Tutorial
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
1112 lines (991 loc) · 48.1 KB
/
index.html
File metadata and controls
1112 lines (991 loc) · 48.1 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
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.8.1: http://docutils.sourceforge.net/" />
<title></title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7056 2011-06-17 10:50:48Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math {
margin-left: 2em ;
margin-right: 2em }
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<div class="section" id="project-status">
<h1>Project Status</h1>
<p>This project is still very much under construction. Any feedback is welcome!</p>
<p>Current progress</p>
<blockquote>
<ul class="simple">
<li>most of Django admin view (p. 2 of tutorial)</li>
<li>first Django model (p. 1 of tutorial)</li>
<li>todo: views, forms (pp 3, 4)</li>
</ul>
</blockquote>
<p>So it's not <em>really</em> ready for someone to use as a proper tutorial yet. If
you're impatient and you want to give it a go anyway, feel free! The selenium
test runner should be of some use at least...</p>
</div>
<div class="section" id="the-concept">
<h1>The Concept</h1>
<p>This idea is to provide an introduction to Test-Driven web development using
Django (and Python). Essentially, we run through the same material as the
official Django tutorial, but instead of 'just' writing code, we write tests
first at each stage - both "functional tests", in which we actually pretend to
be a user, and drive a real web browser, as well as "unit tests", which help us
to design and piece together the individual working parts of the code.</p>
</div>
<div class="section" id="who-is-this-for">
<h1>Who is this for?</h1>
<p>Maybe you've done a bit of Python programming, and you're thinking of learning
Django, and you want to do it "properly". Maybe you've done some test-driven
web development in another language, and you want to find out about how it all
works in the Python world. Most importantly, you've heard of, or had experience
of, working on a project where complexity has started to get the better of you,
where you're scared to make changes, and you wished there had been better
testing from the get-go.</p>
</div>
<div class="section" id="who-is-this-not-for">
<h1>Who is this not for?</h1>
<p>If you know Python, Django and Selenium inside out, I suspect there's better things
that you can do with your time. If you're a total beginner programmer, I also
think it might not be quite right for you - you might do better to get a couple
of other tutorials under your belt first. If you're already a programmer, but
have never tried Python, you'll be fine, but I thoroughly recommend the excellent
"Dive into Python" for a bit more of an insight into the language itself.</p>
</div>
<div class="section" id="why-should-you-listen-to-me">
<h1>Why should you listen to me?</h1>
<p>I was lucky enough to get my first "proper" software development job about a
year ago with a bunch of Extreme Programming fanatics, who've thoroughly
inculcated me into their cult of Test-Driven development. Believe me when I
say I'm contrary enough to have questioned every single practice, challenged
every single decision, moaned about every extra minute spent doing "pointless"
tests instead of writing "proper" code. But I've come round to the idea now,
and whenever I've had to go back to some of my old projects which don't have
tests, boy have I ever realised the wisdom of the approach.</p>
<p>So, I've learnt from some really good people, and the learning process is still
fresh in my mind, so I hope I'll be good at communicating it. Most importantly,
I still have the passion of a recent convert, so I hope I'll be good at conveying
some enthusiasm.</p>
</div>
<div class="section" id="why-test-driven-development">
<h1>Why Test-Driven Development?</h1>
<p>The thing is, when you start out on a small project, you don't really need tests.
Tests take time to write - as much as, if not more than, the actual code for your
application. You've got to learn testing frameworks, and they inevitably come
with a whole host of their own problems (and this applies especially to web-browser
testing. oh boy.). Meanwhile, you know you could just knock out a few lines of
code, and your application would be off the ground, and would start to be
useful. There are deadlines! Clients who are paying for your time! Or maybe
just the smell of that <cite>Internet money</cite>, and arriving late to the party means
none of it will be for you!</p>
<p>Well, that's all true. At first. At first, it's obvious whether everything
works. You can just log into the dev server, click around a bit, and see
whether everything looks OK. And changing this bit of code over <cite>here</cite>, is
only ever going to affect these things <cite>here</cite> and <cite>here</cite>... So it's easy to
change stuff and see if you've broken anything...</p>
<p>But as soon as your project gets slightly larger, complexity rears its ugly
head. Combinatorial explosion starts to make you its bitch. Changes start to
have unpredictable effects. You start to worry about making changes to that
thing over there, because you wrote it ages ago, and you're pretty sure other
things depend on it... best to just use it as it is, even though it's hideously
ugly... Well, anyway, changing this thing over <cite>here</cite> shouldn't affect too much
stuff. I'll just run through the main bits of the site to check... Can't possibly
check everything though... Oh well, I'll just deploy and see if anyone complains...</p>
<p>Automated tests can save you from this fate. If you have automated tests, you can
know for sure whether or not your latest changes broke anything. With tests,
you're free to keep refactoring your code, to keep trying out new ways to optimise
things, to keep adding new functionality, safe in the knowledge that your tests
will let you know if you get things wrong.</p>
<p>Look, that's got to be enough evangelising. If you don't believe me, just ask
someone else with experience. They know. Now, onto the practicals.</p>
</div>
<div class="section" id="what-s-the-approach">
<h1>What's the approach?</h1>
<p>Test-First! So, before we're allowed to write any real production code, we write
some tests. We start by writing some browser tests - what I call <cite>functional</cite>
tests, which simulate what an actual user would see and do. We'll use <cite>Selenium</cite>,
a test tool which actually opens up a real web browser, and then drives it like
a real user, clicking on links and buttons, and checking what is shown on the
screen. These are the tests that will tell us whether or not our application
behaves the way we want it to, from the user's point of view.</p>
<p>Once we've written our functional tests (which, incidentally, have forced us
to thing through the way our application will work, from the point of view
of the user - never a bad thing...) we can start to think about how we want
to implement that functionality from a technical point of view.</p>
<p>Thankfully we won't have to do too much difficult thinking, because the functional
tests will be our guide - what do we have to do to get the functional tests to
get a bit further towards passing? How would we implement that?</p>
<p>Once we've settled on the function or the class that will solve our first problem,
we can write a unit test for it. Again, it forces us to think about how it will
work from the outside, before we write it.</p>
</div>
<div class="section" id="some-setup-before-we-start">
<h1>Some setup before we start</h1>
<p>For functional testing, we'll be using the excellent Selenium. Let's install that,
and Django, and a couple of other Python modules we might need:</p>
<pre class="literal-block">
easy_install django
easy_install selenium
easy_install mock
</pre>
</div>
<div class="section" id="setting-up-our-django-project-and-settings-py">
<h1>Setting up our Django project, and settings.py</h1>
<p>Django structures websites as "projects", each of which can have several
constituent "apps"... Ostensibly, the idea is that apps can be self-contained,
so that you could use one app in several projects... Well, I've never actually
seen that done, but it remains a nice way of splitting up your code.</p>
<p>As per the official Django tutorial, we'll set up our project, and its first app,
a simple application to handle online polls.</p>
<p>Django has a couple of command line tools to set these up:</p>
<pre class="literal-block">
django-admin startproject mysite
cd mysite
./manage.py startapp polls
</pre>
<p>Django stores project-wide settings in a file called <tt class="docutils literal">settings.py</tt>. One of the key
settings is what kind of database to use. We'll use the easiest possible, sqlite.</p>
<p>Find settings <tt class="docutils literal">settings.py</tt> in the root of the new <tt class="docutils literal">mysite</tt> folder, and
open it up in your favourite text editor. Find the lines that mention <tt class="docutils literal">DATABASES</tt>,
and change them, like so:</p>
<pre class="literal-block">
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
'NAME': 'database.sqlite', # Or path to database file if using sqlite3.
</pre>
<p>Find out more about projects, apps and <tt class="docutils literal">settings.py</tt> here:
<a class="reference external" href="https://docs.djangoproject.com/en/1.3/intro/tutorial01/#database-setup">https://docs.djangoproject.com/en/1.3/intro/tutorial01/#database-setup</a></p>
</div>
<div class="section" id="setting-up-the-functional-test-runner">
<h1>Setting up the functional test runner</h1>
<p>The next thing we need is a single command that will run all our FT's, as well
as a folder to keep them all in:</p>
<pre class="literal-block">
mkdir fts
touch fts/__init__.py
</pre>
<p>Here's one I made earlier... A little Python script that'll run all your tests
for you.:</p>
<pre class="literal-block">
wget -O functional_tests.py https://raw.github.com/hjwp/Test-Driven-Django-Tutorial/master/functional_tests.py
chmod +x functional_tests.py
</pre>
<p>We also need to set up a custom set of settings for the FT - we want to make
sure that our tests run against a different copy of the database from the
production one, so that we're not afraid of blowing away real data.</p>
<p>We'll do this by providing an alternative settings file for Django. Create a
file called <tt class="docutils literal">settings_for_fts.py</tt> next to settings.py, and give it the
following contents:</p>
<pre class="literal-block">
from settings import *
DATABASES['default']['NAME'] = 'ft_database.sqlite'
</pre>
<p>That essentially sets up an exact duplicate of the normal <tt class="docutils literal">settings.py</tt>,
except we change the name of the database.</p>
</div>
<div class="section" id="last-bit-of-setup-before-we-start-syncdb">
<h1>Last bit of setup before we start: syncdb</h1>
<p><tt class="docutils literal">syncdb</tt> is the command used to get Django to setup the database. It creates
any new tables that are needed, whenever you've added new stuff to your
project. In this case, it notices it's the first run, and proposes that
you create a superuser. Let's go ahead and do that:</p>
<pre class="literal-block">
python manage.py syncdb
</pre>
<p>Let's use the ultra-secure <tt class="docutils literal">admin</tt> and <tt class="docutils literal">adm1n</tt> as our username and
password for the superuser.:</p>
<pre class="literal-block">
harry@harry-laptop:~/workspace/mysite:master$ ./manage.py syncdb
Username (Leave blank to use 'harry'): admin
E-mail address: admin@example.com
Password:
Password (again):
Superuser created successfully.
</pre>
</div>
<div class="section" id="our-first-test-the-django-admin">
<h1>Our first test: The Django admin</h1>
<p>In the test-driven methodology, we tend to group functionality up into
bite-size chunks, and write functional tests for each one of them. You
can describe the chunks of functionality as "user stories", if you like,
and each user story tends to have a set of tests associated with it,
and the tests track the potential behaviour of a user.</p>
<p>We have to go all the way to the second page of the Django tutorial to see an
actual user-visible part of the application: the <cite>Django admin site</cite>. The
admin site is a really useful part of Django, which generates a UI for site
administrators to manage key bits of information in your database: user
accounts, permissions groups, and, in our case, polls. The admin site will let
admin users create new polls, enter their descriptive text and start and end
dates and so on, before they are published via the user-facing website.
All this stuff comes 'for free' and automatically, just using the Django admin
site.</p>
<p>You can find out more about the philosophy behind the admin site, including Django's
background in the newspaper industry, here:</p>
<p><a class="reference external" href="https://docs.djangoproject.com/en/1.3/intro/tutorial02/">https://docs.djangoproject.com/en/1.3/intro/tutorial02/</a></p>
<p>So, our first user story is that the user should be able to log into the Django
admin site using an admin username and password, and create a new poll.</p>
<img alt="images/admin03t.png" src="images/admin03t.png" />
<img alt="images/admin05t.png" src="images/admin05t.png" />
<p>Let's open up a file inside the <tt class="docutils literal">fts</tt> directory called
<tt class="docutils literal">test_polls_admin.py</tt> and enter the code below.</p>
<p>Note the nice, descriptive names for the test functions, and the comments,
which describe in human-readable text the actions that our user will take.
Mhhhh, descriptive names.....</p>
<p>It's always nice to give the user a name... Mine is called Gertrude...:</p>
<pre class="literal-block">
from functional_tests import FunctionalTest, ROOT
class TestPollsAdmin(FunctionalTest):
def test_can_create_new_poll_via_admin_site(self):
# Gertrude opens her web browser, and goes to the admin page
self.browser.get(ROOT + '/admin/')
# She sees the familiar 'Django administration' heading
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Django administration', body.text)
# She types in her username and passwords and hits return
username_field = self.browser.find_element_by_name('username')
username_field.send_keys('admin')
password_field = self.browser.find_element_by_name('password')
password_field.send_keys('adm1n')
password_field.send_keys(Keys.RETURN)
# She now sees a couple of hyperlink that says "Polls"
polls_links = self.browser.find_elements_by_link_text('Polls')
self.assertEquals(len(polls_links), 2)
# The second one looks more exciting, so she clicks it
polls_links[1].click()
# She is taken to the polls listing page, which shows she has
# no polls yet
body = self.browser.find_element_by_tag_name('body')
self.assertIn('0 polls', body.text)
# She sees a link to 'add' a new poll, so she clicks it
new_poll_link = self.browser.find_element_by_link_text('Add poll')
new_poll_link.click()
#TODO: (we'll write the rest of the test code later)
# She sees some input fields for "Question" and "Publication date"
# She fills these in and clicks "Save" to create the new poll
# She is returned to the "Polls" listing, where she can see her
# new poll
</pre>
<p>Let's try running our first test:</p>
<pre class="literal-block">
./functional_tests.py
</pre>
<p>The test output will looks something like this:</p>
<pre class="literal-block">
Starting Selenium
selenium started
starting django test server
django test server running
running tests
F
======================================================================
FAIL: test_can_create_new_poll_via_admin_site (test_polls_admin.TestPollsAdmin)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/harry/workspace/mysite/fts/test_polls_admin.py", line 12, in test_can_create_new_poll_via_admin_site
self.assertIn('Django administration', body.text)
AssertionError: 'Django administration' not found in u"It worked!\nCongratulations on your first Django-powered page.\nOf course, you haven't actually done any work yet. Here's what to do next:\nIf you plan to use a database, edit the DATABASES setting in mysite/settings.py.\nStart your first app by running python mysite/manage.py startapp [appname].\nYou're seeing this message because you have DEBUG = True in your Django settings file and you haven't configured any URLs. Get to work!"
----------------------------------------------------------------------
Ran 1 test in 4.754s
FAILED (failures=1)
</pre>
</div>
<div class="section" id="first-few-steps">
<h1>First few steps...</h1>
<p>So, let's start trying to get our functional test to pass... or at least get a
little further on. We'll need to set up the Django admin site. This is on
page two of the official Django tutorial:</p>
<p><a class="reference external" href="https://docs.djangoproject.com/en/1.3/intro/tutorial02/#activate-the-admin-site">https://docs.djangoproject.com/en/1.3/intro/tutorial02/#activate-the-admin-site</a></p>
<p>At this point we need to do two things: add "django.contrib.admin" to
INSTALLED_APPS in <tt class="docutils literal">settings.py</tt>:</p>
<pre class="literal-block">
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
# Uncomment the next line to enable the admin:
'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'polls'
)
</pre>
<p>And edit <tt class="docutils literal">mysite/urls.py</tt> to uncomment the lines that reference the admin:</p>
<pre class="literal-block">
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
# [...]
# Uncomment the next line to enable the admin:
url(r'^admin/', include(admin.site.urls)),
)
</pre>
<p>Let's re-run our tests. We should find they get a little further:</p>
<pre class="literal-block">
./functional_tests.py
======================================================================
FAIL: test_can_create_new_poll_via_admin_site (test_polls_admin.TestPollsAdmin)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/harry/workspace/mysite/fts/test_polls_admin.py", line 25, in test_can_create_new_poll_via_admin_site
self.assertEquals(len(polls_links), 2)
AssertionError: 0 != 2
----------------------------------------------------------------------
Ran 1 test in 10.203s
</pre>
<p>Well, the test is happy that there's a Django admin site, and it can log in fine,
but it can't find a link to administer "Polls". So next we need to create the
representation of a Poll inside Django - a <cite>model</cite>, in Django terms.</p>
</div>
<div class="section" id="our-first-unit-tests-testing-a-new-poll-model">
<h1>Our first unit tests: testing a new "Poll" model</h1>
<p>The Django unit test runner will automatically run any tests we put in
<tt class="docutils literal">tests.py</tt>. Later on, we might decide we want to put our tests somewhere
else, but for now, let's use that file:</p>
<pre class="literal-block">
import datetime
from django.test import TestCase
from polls.models import Poll
class TestPollsModel(TestCase):
def test_creating_a_new_poll_and_saving_it_to_the_database(self):
# start by creating a new Poll object with its "question" set
poll = Poll()
poll.question = "What's up?"
# check we can save it to the database
poll.save()
# check we can adjust its publication date
poll.pub_date = datetime.datetime(2012, 12, 25)
poll.save()
# now check we can find it in the database again
all_polls_in_database = Poll.objects.all()
self.assertEquals(len(all_polls_in_database), 1)
only_poll_in_database = all_polls_in_database[0]
self.assertEquals(only_poll_in_database, poll)
# and check that it's saved its two attributes: question and pub_date
self.assertEquals(only_poll_in_database.question, "What's up?")
self.assertEquals(only_poll_in_database.pub_date, poll.pub_date)
</pre>
<p>Unit tests are designed to check that the individual parts of our code work
the way we want them too. Aside from being useful as tests, they're useful
to help us think about the way we design our code... It forces us to think
about how things are going to work, from a slightly external point of view.</p>
<p><go into more detail on how ORM works></p>
<p>Here we're creating a new Poll object, and checking that we can save it to
the database, as well as checking that we can set and store a Poll's main two
attributes: the question and the publication date.:</p>
<pre class="literal-block">
./manage.py test
</pre>
<p>You should see an error like this:</p>
<pre class="literal-block">
File "/usr/local/lib/python2.7/dist-packages/Django/test/simple.py", line 35, in get_tests
test_module = __import__('.'.join(app_path + [TEST_MODULE]), {}, {}, TEST_MODULE)
File "/home/harry/workspace/mysite/polls/tests.py", line 2, in <module>
from polls.models import Poll
ImportError: cannot import name Poll
</pre>
<p>Not the most interesting of test errors - we need to create a Poll object for the
test to import. In TDD, once we've got a test that fails, we're finally allowed
to write some "real" code. But only the minimum required to get the tests to get
a tiny bit further on!</p>
<p>So let's create a minimal Poll class, in <tt class="docutils literal">polls/models.py</tt>:</p>
<pre class="literal-block">
from django.db import models
class Poll(object):
pass
</pre>
<p>And re-run the tests. Pretty soon you'll get into the rhythm of TDD - run the
tests, change a tiny bit of code, check the tests again, see what tiny bit of
code to write next. Run the tests...:</p>
<pre class="literal-block">
Creating test database for alias 'default'...
........................................................................................................................................................................................................................................................................E..........................................................
======================================================================
ERROR: test_creating_a_poll (polls.tests.TestPollsModel)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/harry/workspace/mysite/polls/tests.py", line 8, in test_creating_a_poll
self.assertEquals(poll.name, '')
AttributeError: 'Poll' object has no attribute 'save'
----------------------------------------------------------------------
Ran 323 tests in 2.504s
FAILED (errors=1)
Destroying test database for alias 'default'...
</pre>
<p>Right, the tests are telling us that we can't "save" our Poll. That's because
it's not a Django model object. Let's make the minimal change required to get
our tests further on:</p>
<pre class="literal-block">
class Poll(models.Model):
pass
</pre>
<p>Running the tests again, we should see a slight change to the error message:</p>
<pre class="literal-block">
======================================================================
ERROR: test_creating_a_new_poll_and_saving_it_to_the_database (polls.tests.TestPollsModel)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/harry/workspace/mysite/polls/tests.py", line 26, in test_creating_a_new_poll_and_saving_it_to_the_database
self.assertEquals(only_poll_in_database.question, "What's up?")
AttributeError: 'Poll' object has no attribute 'question'
</pre>
<hr class="docutils" />
<p>Notice that the tests have got all the way through to line 26, where we retrieve
the object back out of the database, and it's telling us that we haven't saved the
question attribute. Let's fix that:</p>
<pre class="literal-block">
class Poll(models.Model):
question = models.CharField(max_length=200)
</pre>
<p><(note on max_length=200)?></p>
<p>Now our tests get slightly further - they tell us we need to add a pub_date:</p>
<pre class="literal-block">
======================================================================
ERROR: test_creating_a_new_poll_and_saving_it_to_the_database (polls.tests.TestPollsModel)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/harry/workspace/mysite/polls/tests.py", line 27, in test_creating_a_new_poll_and_saving_it_to_the_database
self.assertEquals(only_poll_in_database.pub_date, poll.pub_date)
AttributeError: 'Poll' object has no attribute 'pub_date'
----------------------------------------------------------------------
</pre>
<p>Let's add that too:</p>
<pre class="literal-block">
class Poll(models.Model):
question = models.CharField(max_length=200)
pub_date = models.DateTimeField()
</pre>
<p>And run the tests again:</p>
<pre class="literal-block">
...................................................................................................................................................................................................................................................................................................................................
----------------------------------------------------------------------
Ran 323 tests in 2.402s
OK
</pre>
<p>Hooray! The joy of that unbroken string of dots! That lovely, understated "OK".</p>
</div>
<div class="section" id="back-to-the-functional-tests-registering-the-model-with-the-admin-site">
<h1>Back to the functional tests: registering the model with the admin site</h1>
<p>The unit tests all pass. Does this mean our functional test will pass?:</p>
<pre class="literal-block">
./functional_tests.py
======================================================================
FAIL: test_can_create_new_poll_via_admin_site (test_polls_admin.TestPollsAdmin)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/harry/workspace/mysite/fts/test_polls_admin.py", line 25, in test_can_create_new_poll_via_admin_site
self.assertEquals(len(polls_links), 2)
AssertionError: 0 != 2
----------------------------------------------------------------------
Ran 1 test in 10.203s
</pre>
<p>Ah, not quite. The Django admin site doesn't automatically contain every model
you define - you need to tell it which models you want to be able to administer.
To do that, we just need to create a file called <tt class="docutils literal">admin.py</tt> in the <tt class="docutils literal">polls</tt>
directory, with the following three lines:</p>
<pre class="literal-block">
from polls.models import Poll
from django.contrib import admin
admin.site.register(Poll)
</pre>
<p>Let's try the FT again...:</p>
<pre class="literal-block">
./functional_tests.py
.
----------------------------------------------------------------------
Ran 1 test in 6.164s
OK
</pre>
<p>Hooray!</p>
</div>
<div class="section" id="exploring-the-site-manually-using-runserver">
<h1>Exploring the site manually using runserver</h1>
<p>So far so good. But, we still have a few items left as "TODO" in our tests.
At this point we may not be quite sure what we want though. This is a good
time to fire up the Django dev server using <tt class="docutils literal">runserver</tt>, and have a look
around manually, to look for some inspiration on the next steps to take for our
site.:</p>
<pre class="literal-block">
python manage.py runserver
</pre>
<p>Then, open your web browser and go to <tt class="docutils literal"><span class="pre">http://localhost:8000/admin</span></tt>.
Let's follow the steps in the FT - enter the admin username and password (<tt class="docutils literal">adm1n</tt>),
find the link to "Polls' and you'll probably see an error page, in yellow,
that contains something like this:</p>
<pre class="literal-block">
DatabaseError at /admin/polls/poll/
no such table: polls_poll
Request Method: GET
Request URL: http://localhost:8000/admin/polls/poll/
Django Version: 1.3.1
Exception Type: DatabaseError
Exception Value:
no such table: polls_poll
Exception Location: /usr/local/lib/python2.7/dist-packages/django/db/backends/sqlite3/base.py in execute, line 234
Python Executable: /usr/bin/python
Python Version: 2.7.1
[etc]
</pre>
<p>When Django encounters an error trying to render a page, it displays a page
full of debugging information like this, to help you figure out what went
wrong.</p>
<p>When your application is ready to show to real users, you'll want to set
<tt class="docutils literal">DEBUG = False</tt> in your <tt class="docutils literal">settings.py</tt>, because you don't want your users
seeing that sort of information (Django can email it to you instead). In the
meantime, it's very useful!</p>
<p>So, Django is telling us it can't find a database table called <tt class="docutils literal">poll_poll</tt>.</p>
<p>Django names tables using the convention <tt class="docutils literal">appname_lowercasemodelname</tt>, so
this is the table for our Poll object, and we haven't told Django to create it
for us yet. "What about the tests", I hear you ask, "they seemed to run
fine?!". Well, if you remember we set up a different database for our FTs, and
the the Django unit test runner also uses its own database.</p>
<p>So, as far as your production database is concerned, you'll need to run
<tt class="docutils literal">syncdb</tt> manually, each time you create a new class in <tt class="docutils literal">models.py</tt> . Press
Ctrl+C to quit the test server, and then:</p>
<pre class="literal-block">
python manage.py syncb
Creating tables ...
Creating table polls_poll
Installing custom SQL ...
Installing indexes ...
No fixtures found.
</pre>
</div>
<div class="section" id="inspecting-the-admin-site-to-decide-what-to-test-next">
<h1>Inspecting the admin site to decide what to test next</h1>
<p>Let's run <tt class="docutils literal">python manage.py runserver</tt> again, and go take another look at the
admin pages. If you try and create a new Poll, you should see a menu a bit like
this.</p>
<p><insert screenshot></p>
<p>Pretty neat, but <cite>Pub date</cite> isn't a very nice label for our publication date
field. Django normally generates labels for its admin fields automatically,
by just taking the field name and capitalising it, converting underscores
to spaces. So that works well for <tt class="docutils literal">question</tt>, but not so well for <tt class="docutils literal">pub_date</tt>.</p>
<p>So that's one thing we'll want to change. Let's add a test for that to the end of
our FT:</p>
<pre class="literal-block">
# She sees a link to 'add' a new poll, so she clicks it
new_poll_link = self.browser.find_element_by_link_text('Add poll')
new_poll_link.click()
# She sees some input fields for "Question" and "Date published"
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Question:', body.text)
self.assertIn('Date published:', body.text)
</pre>
</div>
<div class="section" id="more-ways-of-finding-elements-on-the-page-using-selenium">
<h1>More ways of finding elements on the page using Selenium</h1>
<p>Try filling in a new Poll, and fill in the 'date' entry but not a 'time'. You'll
find django complains that the field is required. So, in our test, we need to
fill in three fields: <cite>question</cite>, <cite>date</cite>, and <cite>time</cite>.</p>
<p>In order to get Selenium to find the text input boxes for those fields, there
are several options:</p>
<pre class="literal-block">
find_element_by_id
find_element_by_xpath
find_element_by_link_text
find_element_by_name
find_element_by_tag_name
find_element_by_css_selector
</pre>
<p>And several others - the Selenium Webdriver documentation is still a bit sparse,
but you can look at the source code, and most of the methods have fairly self-
explanatory names...</p>
<p><a class="reference external" href="http://code.google.com/p/selenium/source/browse/trunk/py/selenium/webdriver/remote/webdriver.py">http://code.google.com/p/selenium/source/browse/trunk/py/selenium/webdriver/remote/webdriver.py</a></p>
<p>In our case <cite>by name</cite> is a useful way of finding fields, because the name
attribute is usually associated with input fields from forms. If you take a
look at the HTML source code for the Django admin page for entering a new poll
(either the raw source, or using a tool like Firebug, or developer tools in
Google Chrome), you'll find out that the 'name' for our three fields are
<cite>question</cite>, <cite>pub_date_0</cite> and <cite>pub_date_1</cite>.:</p>
<pre class="literal-block">
<label for="id_question" class="required">Question:</label>
<input id="id_question" type="text" class="vTextField" name="question" maxlength="200" />
<label for="id_pub_date_0" class="required">Date published:</label>
<p class="datetime">
Date:
<input id="id_pub_date_0" type="text" class="vDateField" name="pub_date_0" size="10" />
<br />
Time:
<input id="id_pub_date_1" type="text" class="vTimeField" name="pub_date_1" size="8" />
</p>
</pre>
<p>Let's use them in our FT:</p>
<pre class="literal-block">
# She sees some input fields for "Question" and "Date published"
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Question:', body.text)
self.assertIn('Date published:', body.text)
# She types in an interesting question for the Poll
question_field = self.browser.find_element_by_name('question')
question_field.send_keys("How awesome is Test-Driven Development?")
# She sets the date and time of publication - it'll be a new year's
# poll!
date_field = self.browser.find_element_by_name('pub_date_0')
date_field.send_keys('01/01/12')
time_field = self.browser.find_element_by_name('pub_date_1')
time_field.send_keys('00:00')
</pre>
<p>We can also use the CSS selector to pick up the "Save" button:</p>
<pre class="literal-block">
save_button = self.browser.find_element_by_css_selector("input[value='Save']")
save_button.click()
</pre>
<p>Finally, we'll want to have our test check that the new Poll appears on the listings
page. If you've entered a Poll, you'll have noticed that the polls are just described
as "Poll object".</p>
<p><insert screenshot(s)></p>
<p>Django lets you give them more descriptive names, including any attribute of
the object. So let's say we want our polls listed by their question:</p>
<pre class="literal-block">
# She is returned to the "Polls" listing, where she can see her
# new poll, listed as a clickable link
new_poll_links = self.browser.find_elements_by_link_text(
"How awesome is Test-Driven Development?"
)
self.assertEquals(len(new_poll_links), 1)
</pre>
<p>That's our FT finished. If you've lost track in amongst all the copy & pasting,
you can compare your version to mine, which is hosted here:
<a class="reference external" href="https://github.com/hjwp/Test-Driven-Django-Tutorial/blob/master/fts/test_polls_admin.py">https://github.com/hjwp/Test-Driven-Django-Tutorial/blob/master/fts/test_polls_admin.py</a></p>
</div>
<div class="section" id="human-readable-names-for-models-and-their-attributes">
<h1>Human-readable names for models and their attributes</h1>