Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #9962 - Added a testing tutorial.

Thank-you Daniele Procida for the first draft
and shaibi, Aymeric, and others for the reviews.
  • Loading branch information...
commit b052e6cc959b540e4d1de8a747689c25c6aeace9 1 parent 3984ccb
Tim Graham authored December 15, 2012
3  docs/index.txt
@@ -44,7 +44,8 @@ Are you new to Django or to programming? This is the place to start!
44 44
   :doc:`Part 1 <intro/tutorial01>` |
45 45
   :doc:`Part 2 <intro/tutorial02>` |
46 46
   :doc:`Part 3 <intro/tutorial03>` |
47  
-  :doc:`Part 4 <intro/tutorial04>`
  47
+  :doc:`Part 4 <intro/tutorial04>` |
  48
+  :doc:`Part 5 <intro/tutorial05>`
48 49
 
49 50
 * **Advanced Tutorials:**
50 51
   :doc:`How to write reusable apps <intro/reusable-apps>` |
1  docs/intro/index.txt
@@ -13,6 +13,7 @@ place: read this material to quickly get up and running.
13 13
    tutorial02
14 14
    tutorial03
15 15
    tutorial04
  16
+   tutorial05
16 17
    reusable-apps
17 18
    whatsnext
18 19
    contributing
4  docs/intro/reusable-apps.txt
@@ -2,11 +2,11 @@
2 2
 Advanced tutorial: How to write reusable apps
3 3
 =============================================
4 4
 
5  
-This advanced tutorial begins where :doc:`Tutorial 4 </intro/tutorial04>` left
  5
+This advanced tutorial begins where :doc:`Tutorial 5 </intro/tutorial05>` left
6 6
 off. We'll be turning our Web-poll into a standalone Python package you can
7 7
 reuse in new projects and share with other people.
8 8
 
9  
-If you haven't recently completed Tutorials 1–4, we encourage you to review
  9
+If you haven't recently completed Tutorials 1–5, we encourage you to review
10 10
 these so that your example project matches the one described below.
11 11
 
12 12
 Reusability matters
12  docs/intro/tutorial04.txt
@@ -275,13 +275,5 @@ Run the server, and use your new polling app based on generic views.
275 275
 For full details on generic views, see the :doc:`generic views documentation
276 276
 </topics/class-based-views/index>`.
277 277
 
278  
-What's next?
279  
-============
280  
-
281  
-The beginner tutorial ends here for the time being. In the meantime, you might
282  
-want to check out some pointers on :doc:`where to go from here
283  
-</intro/whatsnext>`.
284  
-
285  
-If you are familiar with Python packaging and interested in learning how to
286  
-turn polls into a "reusable app", check out :doc:`Advanced tutorial: How to
287  
-write reusable apps</intro/reusable-apps>`.
  278
+When you're comfortable with forms and generic views, read :doc:`part 5 of this
  279
+tutorial</intro/tutorial05>` to learn about testing our polls app.
650  docs/intro/tutorial05.txt
... ...
@@ -0,0 +1,650 @@
  1
+=====================================
  2
+Writing your first Django app, part 5
  3
+=====================================
  4
+
  5
+This tutorial begins where :doc:`Tutorial 4 </intro/tutorial04>` left off.
  6
+We've built a Web-poll application, and we'll now create some automated tests
  7
+for it.
  8
+
  9
+Introducing automated testing
  10
+=============================
  11
+
  12
+What are automated tests?
  13
+-------------------------
  14
+
  15
+Tests are simple routines that check the operation of your code.
  16
+
  17
+Testing operates at different levels. Some tests might apply to a tiny detail
  18
+- *does a particular model method return values as expected?*, while others
  19
+examine the overall operation of the software - *does a sequence of user inputs
  20
+on the site produce the desired result?* That's no different from the kind of
  21
+testing you did earlier in :doc:`Tutorial 1 </intro/tutorial01>`, using the
  22
+shell to examine the behavior of a method, or running the application and
  23
+entering data to check how it behaves.
  24
+
  25
+What's different in *automated* tests is that the testing work is done for
  26
+you by the system. You create a set of tests once, and then as you make changes
  27
+to your app, you can check that your code still works as you originally
  28
+intended, without having to perform time consuming manual testing.
  29
+
  30
+Why you need to create tests
  31
+----------------------------
  32
+
  33
+So why create tests, and why now?
  34
+
  35
+You may feel that you have quite enough on your plate just learning
  36
+Python/Django, and having yet another thing to learn and do may seem
  37
+overwhelming and perhaps unnecessary. After all, our polls application is
  38
+working quite happily now; going through the trouble of creating automated
  39
+tests is not going to make it work any better. If creating the polls
  40
+application is the last bit of Django programming you will ever do, then true,
  41
+you don't need to know how to create automated tests. But, if that's not the
  42
+case, now is an excellent time to learn.
  43
+
  44
+Tests will save you time
  45
+~~~~~~~~~~~~~~~~~~~~~~~~
  46
+
  47
+Up to a certain point, 'checking that it seems to work' will be a satisfactory
  48
+test. In a more sophisticated application, you might have dozens of complex
  49
+interactions between components.
  50
+
  51
+A change in any of those components could have unexpected consequences on the
  52
+application's behavior. Checking that it still 'seems to work' could mean
  53
+running through your code's functionality with twenty different variations of
  54
+your test data just to make sure you haven't broken something - not a good use
  55
+of your time.
  56
+
  57
+That's especially true when automated tests could do this for you in seconds.
  58
+If something's gone wrong, tests will also assist in identifying the code
  59
+that's causing the unexpected behavior.
  60
+
  61
+Sometimes it may seem a chore to tear yourself away from your productive,
  62
+creative programming work to face the unglamorous and unexciting business
  63
+of writing tests, particularly when you know your code is working properly.
  64
+
  65
+However, the task of writing tests is a lot more fulfilling than spending hours
  66
+testing your application manually or trying to identify the cause of a
  67
+newly-introduced problem.
  68
+
  69
+Tests don't just identify problems, they prevent them
  70
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  71
+
  72
+It's a mistake to think of tests merely as a negative aspect of development.
  73
+
  74
+Without tests, the purpose or intended behavior of an application might be
  75
+rather opaque. Even when it's your own code, you will sometimes find yourself
  76
+poking around in it trying to find out what exactly it's doing.
  77
+
  78
+Tests change that; they light up your code from the inside, and when something
  79
+goes wrong, they focus light on the part that has gone wrong - *even if you
  80
+hadn't even realized it had gone wrong*.
  81
+
  82
+Tests make your code more attractive
  83
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  84
+
  85
+You might have created a brilliant piece of software, but you will find that
  86
+many other developers will simply refuse to look at it because it lacks tests;
  87
+without tests, they won't trust it. Jacob Kaplan-Moss, one of Django's
  88
+original developers, says "Code without tests is broken by design."
  89
+
  90
+That other developers want to see tests in your software before they take it
  91
+seriously is yet another reason for you to start writing tests.
  92
+
  93
+Tests help teams work together
  94
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  95
+
  96
+The previous points are written from the point of view of a single developer
  97
+maintaining an application. Complex applications will be maintained by teams.
  98
+Tests guarantee that colleagues don't inadvertently break your code (and that
  99
+you don't break theirs without knowing). If you want to make a living as a
  100
+Django programmer, you must be good at writing tests!
  101
+
  102
+Basic testing strategies
  103
+========================
  104
+
  105
+There are many ways to approach writing tests.
  106
+
  107
+Some programmers follow a discipline called "`test-driven development`_"; they
  108
+actually write their tests before they write their code. This might seem
  109
+counter-intuitive, but in fact it's similar to what most people will often do
  110
+anyway: they describe a problem, then create some code to solve it. Test-driven
  111
+development simply formalizes the problem in a Python test case.
  112
+
  113
+More often, a newcomer to testing will create some code and later decide that
  114
+it should have some tests. Perhaps it would have been better to write some
  115
+tests earlier, but it's never too late to get started.
  116
+
  117
+Sometimes it's difficult to figure out where to get started with writing tests.
  118
+If you have written several thousand lines of Python, choosing something to
  119
+test might not be easy. In such a case, it's fruitful to write your first test
  120
+the next time you make a change, either when you add a new feature or fix a bug.
  121
+
  122
+So let's do that right away.
  123
+
  124
+.. _test-driven development: http://en.wikipedia.org/wiki/Test-driven_development/
  125
+
  126
+Writing our first test
  127
+======================
  128
+
  129
+We identify a bug
  130
+-----------------
  131
+
  132
+Fortunately, there's a little bug in the ``polls`` application for us to fix
  133
+right away: the ``Poll.was_published_recently()`` method returns ``True`` if
  134
+the ``Poll`` was published within the last day (which is correct) but also if
  135
+the ``Poll``'s ``pub_date`` field is in the future (which certainly isn't).
  136
+
  137
+You can see this in the Admin; create a poll whose date lies in the future;
  138
+you'll see that the ``Poll`` change list claims it was published recently.
  139
+
  140
+You can also see this using the shell::
  141
+
  142
+    >>> import datetime
  143
+    >>> from django.utils import timezone
  144
+    >>> from polls.models import Poll
  145
+    >>> # create a Poll instance with pub_date 30 days in the future
  146
+    >>> future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
  147
+    >>> # was it published recently?
  148
+    >>> future_poll.was_published_recently()
  149
+    True
  150
+
  151
+Since things in the future are not 'recent', this is clearly wrong.
  152
+
  153
+Create a test to expose the bug
  154
+-------------------------------
  155
+
  156
+What we've just done in the shell to test for the problem is exactly what we
  157
+can do in an automated test, so let's turn that into an automated test.
  158
+
  159
+The best place for an application's tests is in the application's ``tests.py``
  160
+file - the testing system will look there for tests automatically.
  161
+
  162
+Put the following in the ``tests.py`` file in the ``polls`` application (you'll
  163
+notice  ``tests.py`` contains some dummy tests, you can remove those)::
  164
+
  165
+    import datetime
  166
+
  167
+    from django.utils import timezone
  168
+    from django.test import TestCase
  169
+
  170
+    from polls.models import Poll
  171
+
  172
+    class PollMethodTests(TestCase):
  173
+
  174
+        def test_was_published_recently_with_future_poll(self):
  175
+            """
  176
+            was_published_recently() should return False for polls whose
  177
+            pub_date is in the future
  178
+            """
  179
+            future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
  180
+            self.assertEqual(future_poll.was_published_recently(), False)
  181
+
  182
+What we have done here is created a :class:`django.test.TestCase` subclass
  183
+with a method that creates a ``Poll`` instance with a ``pub_date`` in the
  184
+future. We then check the output of ``was_published_recently()`` - which
  185
+*ought* to be False.
  186
+
  187
+Running tests
  188
+-------------
  189
+
  190
+In the terminal, we can run our test::
  191
+
  192
+    python manage.py test polls
  193
+
  194
+and you'll see something like::
  195
+
  196
+    Creating test database for alias 'default'...
  197
+    F
  198
+    ======================================================================
  199
+    FAIL: test_was_published_recently_with_future_poll (polls.tests.PollMethodTests)
  200
+    ----------------------------------------------------------------------
  201
+    Traceback (most recent call last):
  202
+      File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_poll
  203
+        self.assertEqual(future_poll.was_published_recently(), False)
  204
+    AssertionError: True != False
  205
+
  206
+    ----------------------------------------------------------------------
  207
+    Ran 1 test in 0.001s
  208
+
  209
+    FAILED (failures=1)
  210
+    Destroying test database for alias 'default'...
  211
+
  212
+What happened is this:
  213
+
  214
+* ``python manage.py test polls`` looked for tests in the ``polls`` application
  215
+
  216
+* it found a subclass of the :class:`django.test.TestCase` class
  217
+
  218
+* it created a special database for the purpose of testing
  219
+
  220
+* it looked for test methods - ones whose names begin with ``test``
  221
+
  222
+* in ``test_was_published_recently_with_future_poll`` it created a ``Poll``
  223
+  instance whose ``pub_date`` field is 30 days in the future
  224
+
  225
+* ... and using the ``assertEqual()`` method, it discovered that its
  226
+  ``was_published_recently()`` returns ``True``, though we wanted it to return
  227
+  ``False``
  228
+
  229
+The test informs us which test failed and even the line on which the failure
  230
+occurred.
  231
+
  232
+Fixing the bug
  233
+--------------
  234
+
  235
+We already know what the problem is: ``Poll.was_published_recently()`` should
  236
+return ``False`` if its ``pub_date`` is in the future. Amend the method in
  237
+``models.py``, so that it will only return ``True`` if the date is also in the
  238
+past::
  239
+
  240
+    def was_published_recently(self):
  241
+        now = timezone.now()
  242
+        return now - datetime.timedelta(days=1) <= self.pub_date <  now
  243
+
  244
+and run the test again::
  245
+
  246
+    Creating test database for alias 'default'...
  247
+    .
  248
+    ----------------------------------------------------------------------
  249
+    Ran 1 test in 0.001s
  250
+
  251
+    OK
  252
+    Destroying test database for alias 'default'...
  253
+
  254
+After identifying a bug, we wrote a test that exposes it and corrected the bug
  255
+in the code so our test passes.
  256
+
  257
+Many other things might go wrong with our application in the future, but we can
  258
+be sure that we won't inadvertently reintroduce this bug, because simply
  259
+running the test will warn us immediately. We can consider this little portion
  260
+of the application pinned down safely forever.
  261
+
  262
+More comprehensive tests
  263
+------------------------
  264
+
  265
+While we're here, we can further pin down the ``was_published_recently()``
  266
+method; in fact, it would be positively embarrassing if in fixing one bug we had
  267
+introduced another.
  268
+
  269
+Add two more test methods to the same class, to test the behavior of the method
  270
+more comprehensively::
  271
+
  272
+    def test_was_published_recently_with_old_poll(self):
  273
+        """
  274
+        was_published_recently() should return False for polls whose pub_date
  275
+        is older than 1 day
  276
+        """
  277
+        old_poll = Poll(pub_date=timezone.now() - datetime.timedelta(days=30))
  278
+        self.assertEqual(old_poll.was_published_recently(), False)
  279
+
  280
+    def test_was_published_recently_with_recent_poll(self):
  281
+        """
  282
+        was_published_recently() should return True for polls whose pub_date
  283
+        is within the last day
  284
+        """
  285
+        recent_poll = Poll(pub_date=timezone.now() - datetime.timedelta(hours=1))
  286
+        self.assertEqual(recent_poll.was_published_recently(), True)
  287
+
  288
+And now we have three tests that confirm that ``Poll.was_published_recently()``
  289
+returns sensible values for past, recent, and future polls.
  290
+
  291
+Again, ``polls`` is a simple application, but however complex it grows in the
  292
+future and whatever other code it interacts with, we now have some guarantee
  293
+that the method we have written tests for will behave in expected ways.
  294
+
  295
+Test a view
  296
+===========
  297
+
  298
+The polls application is fairly undiscriminating: it will publish any poll,
  299
+including ones whose ``pub_date`` field lies in the future. We should improve
  300
+this. Setting a ``pub_date`` in the future should mean that the Poll is
  301
+published at that moment, but invisible until then.
  302
+
  303
+A test for a view
  304
+-----------------
  305
+
  306
+When we fixed the bug above, we wrote the test first and then the code to fix
  307
+it. In fact that was a simple example of test-driven development, but it
  308
+doesn't really matter in which order we do the work.
  309
+
  310
+In our first test, we focused closely on the internal behavior of the code. For
  311
+this test, we want to check its behavior as it would be experienced by a user
  312
+through a web browser.
  313
+
  314
+Before we try to fix anything, let's have a look at the tools at our disposal.
  315
+
  316
+The Django test client
  317
+----------------------
  318
+
  319
+Django provides a test :class:`~django.test.client.Client` to simulate a user
  320
+interacting with the code at the view level.  We can use it in ``tests.py``
  321
+or even in the shell.
  322
+
  323
+We will start again with the shell, where we need to do a couple of things that
  324
+won't be necessary in ``tests.py``. The first is to set up the test environment
  325
+in the shell::
  326
+
  327
+    >>> from django.test.utils import setup_test_environment
  328
+    >>> setup_test_environment()
  329
+
  330
+Next we need to import the test client class (later in ``tests.py`` we will use
  331
+the :class:`django.test.TestCase` class, which comes with its own client, so
  332
+this won't be required)::
  333
+
  334
+    >>> from django.test.client import Client
  335
+    >>> # create an instance of the client for our use
  336
+    >>> client = Client()
  337
+
  338
+With that ready, we can ask the client to do some work for us::
  339
+
  340
+    >>> # get a response from '/'
  341
+    >>> response = client.get('/')
  342
+    >>> # we should expect a 404 from that address
  343
+    >>> response.status_code
  344
+    404
  345
+    >>> # on the other hand we should expect to find something at '/polls/'
  346
+    >>> # we'll use 'reverse()' rather than a harcoded URL
  347
+    >>> from django.core.urlresolvers import reverse
  348
+    >>> response = client.get(reverse('polls:index'))
  349
+    >>> response.status_code
  350
+    200
  351
+    >>> response.content
  352
+    '\n\n\n    <p>No polls are available.</p>\n\n'
  353
+    >>> # note - you might get unexpected results if your ``TIME_ZONE``
  354
+    >>> # in ``settings.py`` is not correct. If you need to change it,
  355
+    >>> # you will also need to restart your shell session
  356
+    >>> from polls.models import Poll
  357
+    >>> from django.utils import timezone
  358
+    >>> # create a Poll and save it
  359
+    >>> p = Poll(question="Who is your favorite Beatle?", pub_date=timezone.now())
  360
+    >>> p.save()
  361
+    >>> # check the response once again
  362
+    >>> response = client.get('/polls/')
  363
+    >>> response.content
  364
+    '\n\n\n    <ul>\n    \n        <li><a href="/polls/1/">Who is your favorite Beatle?</a></li>\n    \n    </ul>\n\n'
  365
+    >>> response.context['latest_poll_list']
  366
+    [<Poll: Who is your favorite Beatle?>]
  367
+
  368
+Improving our view
  369
+------------------
  370
+
  371
+The list of polls shows polls that aren't published yet (i.e. those that have a
  372
+``pub_date`` in the future). Let's fix that.
  373
+
  374
+In :doc:`Tutorial 4 </intro/tutorial04>` we deleted the view functions from
  375
+``views.py`` in favor of a :class:`~django.views.generic.list.ListView` in
  376
+``urls.py``::
  377
+
  378
+    url(r'^$',
  379
+        ListView.as_view(
  380
+            queryset=Poll.objects.order_by('-pub_date')[:5],
  381
+            context_object_name='latest_poll_list',
  382
+            template_name='polls/index.html'),
  383
+        name='index'),
  384
+
  385
+``response.context_data['latest_poll_list']`` extracts the data this view
  386
+places into the context.
  387
+
  388
+We need to amend the line that gives us the ``queryset``::
  389
+
  390
+    queryset=Poll.objects.order_by('-pub_date')[:5],
  391
+
  392
+Let's change the queryset so that it also checks the date by comparing it with
  393
+``timezone.now()``. First we need to add an import::
  394
+
  395
+    from django.utils import timezone
  396
+
  397
+and then we must amend the existing ``url`` function to::
  398
+
  399
+    url(r'^$',
  400
+        ListView.as_view(
  401
+            queryset=Poll.objects.filter(pub_date__lte=timezone.now) \
  402
+                .order_by('-pub_date')[:5],
  403
+            context_object_name='latest_poll_list',
  404
+            template_name='polls/index.html'),
  405
+        name='index'),
  406
+
  407
+``Poll.objects.filter(pub_date__lte=timezone.now)`` returns a queryset
  408
+containing Polls whose ``pub_date`` is less than or equal to - that is, earlier
  409
+than or equal to - ``timezone.now``. Notice that we use a callable queryset
  410
+argument, ``timezone.now``, which will be evaluated at request time. If we had
  411
+included the parentheses, ``timezone.now()`` would be evaluated just once when
  412
+the web server is started.
  413
+
  414
+Testing our new view
  415
+--------------------
  416
+
  417
+Now you can satisfy yourself that this behaves as expected by firing up the
  418
+runserver, loading the site in your browser, creating ``Polls`` with dates in
  419
+the past and future, and checking that only those that have been published are
  420
+listed.  You don't want to have to do that *every single time you make any
  421
+change that might affect this* - so let's also create a test, based on our
  422
+shell session above.
  423
+
  424
+Add the following to ``polls/tests.py``::
  425
+
  426
+    from django.core.urlresolvers import reverse
  427
+
  428
+and we'll create a factory method to create polls as well as a new test class::
  429
+
  430
+    def create_poll(question, days):
  431
+        """
  432
+        Creates a poll with the given `question` published the given number of
  433
+        `days` offset to now (negative for polls published in the past,
  434
+        positive for polls that have yet to be published).
  435
+        """
  436
+        return Poll.objects.create(question=question,
  437
+            pub_date=timezone.now() + datetime.timedelta(days=days))
  438
+
  439
+    class PollViewTests(TestCase):
  440
+        def test_index_view_with_no_polls(self):
  441
+            """
  442
+            If no polls exist, an appropriate message should be displayed.
  443
+            """
  444
+            response = self.client.get(reverse('polls:index'))
  445
+            self.assertEqual(response.status_code, 200)
  446
+            self.assertContains(response, "No polls are available.")
  447
+            self.assertQuerysetEqual(response.context['latest_poll_list'], [])
  448
+
  449
+        def test_index_view_with_a_past_poll(self):
  450
+            """
  451
+            Polls with a pub_date in the past should be displayed on the index page.
  452
+            """
  453
+            create_poll(question="Past poll.", days=-30)
  454
+            response = self.client.get(reverse('polls:index'))
  455
+            self.assertQuerysetEqual(
  456
+                response.context['latest_poll_list'],
  457
+                ['<Poll: Past poll.>']
  458
+            )
  459
+
  460
+        def test_index_view_with_a_future_poll(self):
  461
+            """
  462
+            Polls with a pub_date in the future should not be displayed on the
  463
+            index page.
  464
+            """
  465
+            create_poll(question="Future poll.", days=30)
  466
+            response = self.client.get(reverse('polls:index'))
  467
+            self.assertContains(response, "No polls are available.", status_code=200)
  468
+            self.assertQuerysetEqual(response.context['latest_poll_list'], [])
  469
+
  470
+        def test_index_view_with_future_poll_and_past_poll(self):
  471
+            """
  472
+            Even if both past and future polls exist, only past polls should be
  473
+            displayed.
  474
+            """
  475
+            create_poll(question="Past poll.", days=-30)
  476
+            create_poll(question="Future poll.", days=30)
  477
+            response = self.client.get(reverse('polls:index'))
  478
+            self.assertQuerysetEqual(
  479
+                response.context['latest_poll_list'],
  480
+                ['<Poll: Past poll.>']
  481
+            )
  482
+
  483
+        def test_index_view_with_two_past_polls(self):
  484
+            """
  485
+            The polls index page may display multiple polls.
  486
+            """
  487
+            create_poll(question="Past poll 1.", days=-30)
  488
+            create_poll(question="Past poll 2.", days=-5)
  489
+            response = self.client.get(reverse('polls:index'))
  490
+            self.assertQuerysetEqual(
  491
+                response.context['latest_poll_list'],
  492
+                 ['<Poll: Past poll 2.>', '<Poll: Past poll 1.>']
  493
+            )
  494
+
  495
+Let's look at some of these more closely.
  496
+
  497
+First is a poll factory method, ``create_poll``, to take some repetition out
  498
+of the process of creating polls.
  499
+
  500
+``test_index_view_with_no_polls`` doesn't create any polls, but checks the
  501
+message: "No polls are available." and verifies the ``latest_poll_list`` is
  502
+empty. Note that the :class:`django.test.TestCase` class provides some
  503
+additional assertion methods. In these examples, we use
  504
+:meth:`~django.test.TestCase.assertContains()` and
  505
+:meth:`~django.test.TestCase.assertQuerysetEqual()`.
  506
+
  507
+In ``test_index_view_with_a_past_poll``, we create a poll and verify that it
  508
+appears in the list.
  509
+
  510
+In ``test_index_view_with_a_future_poll``, we create a poll with a ``pub_date``
  511
+in the future. The database is reset for each test method, so the first poll is
  512
+no longer there, and so again the index shouldn't have any polls in it.
  513
+
  514
+And so on. In effect, we are using the tests to tell a story of admin input
  515
+and user experience on the site, and checking that at every state and for every
  516
+new change in the state of the system, the expected results are published.
  517
+
  518
+Testing the ``DetailView``
  519
+--------------------------
  520
+
  521
+What we have works well; however, even though future polls don't appear in the
  522
+*index*, users can still reach them if they know or guess the right URL. So we
  523
+need similar constraints in the ``DetailViews``, by adding::
  524
+
  525
+    queryset=Poll.objects.filter(pub_date__lte=timezone.now)
  526
+
  527
+to them - for example::
  528
+
  529
+    url(r'^(?P<pk>\d+)/$',
  530
+        DetailView.as_view(
  531
+            queryset=Poll.objects.filter(pub_date__lte=timezone.now),
  532
+            model=Poll,
  533
+            template_name='polls/detail.html'),
  534
+        name='detail'),
  535
+
  536
+and of course, we will add some tests, to check that a ``Poll`` whose
  537
+``pub_date`` is in the past can be displayed, and that one with a ``pub_date``
  538
+in the future is not::
  539
+
  540
+    class PollIndexDetailTests(TestCase):
  541
+        def test_detail_view_with_a_future_poll(self):
  542
+            """
  543
+            The detail view of a poll with a pub_date in the future should
  544
+            return a 404 not found.
  545
+            """
  546
+            future_poll = create_poll(question='Future poll.', days=5)
  547
+            response = self.client.get(reverse('polls:detail', args=(future_poll.id,)))
  548
+            self.assertEqual(response.status_code, 404)
  549
+
  550
+        def test_detail_view_with_a_past_poll(self):
  551
+            """
  552
+            The detail view of a poll with a pub_date in the past should display
  553
+            the poll's question.
  554
+            """
  555
+            past_poll = create_poll(question='Past Poll.', days=-5)
  556
+            response = self.client.get(reverse('polls:detail', args=(past_poll.id,)))
  557
+            self.assertContains(response, past_poll.question, status_code=200)
  558
+
  559
+Ideas for more tests
  560
+--------------------
  561
+
  562
+We ought to add similar ``queryset`` arguments to the other ``DetailView``
  563
+URLs, and create a new test class for each view. They'll be very similar to
  564
+what we have just created; in fact there will be a lot of repetition.
  565
+
  566
+We could also improve our application in other ways, adding tests along the
  567
+way. For example, it's silly that ``Polls`` can be published on the site that
  568
+have no ``Choices``. So, our views could check for this, and exclude such
  569
+``Polls``. Our tests would create a ``Poll`` without ``Choices`` and then test
  570
+that it's not published, as well as create a similar ``Poll`` *with*
  571
+``Choices``, and test that it *is* published.
  572
+
  573
+Perhaps logged-in admin users should be allowed to see unpublished ``Polls``,
  574
+but not ordinary visitors. Again: whatever needs to be added to the software to
  575
+accomplish this should be accompanied by a test, whether you write the test
  576
+first and then make the code pass the test, or work out the logic in your code
  577
+first and then write a test to prove it.
  578
+
  579
+At a certain point you are bound to look at your tests and wonder whether your
  580
+code is suffering from test bloat, which brings us to:
  581
+
  582
+When testing, more is better
  583
+============================
  584
+
  585
+It might seem that our tests are growing out of control. At this rate there will
  586
+soon be more code in our tests than in our application, and the repetition
  587
+is unaesthetic, compared to the elegant conciseness of the rest of our code.
  588
+
  589
+**It doesn't matter**. Let them grow. For the most part, you can write a test
  590
+once and then forget about it. It will continue performing its useful function
  591
+as you continue to develop your program.
  592
+
  593
+Sometimes tests will need to be updated. Suppose that we amend our views so that
  594
+only ``Polls`` with ``Choices`` are published. In that case, many of our
  595
+existing tests will fail - *telling us exactly which tests need to be amended to
  596
+bring them up to date*, so to that extent tests help look after themselves.
  597
+
  598
+At worst, as you continue developing, you might find that you have some tests
  599
+that are now redundant. Even that's not a problem; in testing redundancy is
  600
+a *good* thing.
  601
+
  602
+As long as your tests are sensibly arranged, they won't become unmanageable.
  603
+Good rules-of-thumb include having:
  604
+
  605
+* a separate ``TestClass`` for each model or view
  606
+* a separate test method for each set of conditions you want to test
  607
+* test method names that describe their function
  608
+
  609
+Further testing
  610
+===============
  611
+
  612
+This tutorial only introduces some of the basics of testing. There's a great
  613
+deal more you can do, and a number of very useful tools at your disposal to
  614
+achieve some very clever things.
  615
+
  616
+For example, while our tests here have covered some of the internal logic of a
  617
+model and the way our views publish information, you can use an "in-browser"
  618
+framework such as Selenium_ to test the way your HTML actually renders in a
  619
+browser. These tools allow you to check not just the behavior of your Django
  620
+code, but also, for example, of your JavaScript. It's quite something to see
  621
+the tests launch a browser, and start interacting with your site, as if a human
  622
+being were driving it! Django includes :class:`~django.test.LiveServerTestCase`
  623
+to facilitate integration with tools like Selenium.
  624
+
  625
+If you have a complex application, you may want to run tests automatically
  626
+with every commit for the purposes of `continuous integration`_, so that
  627
+quality control is itself - at least partially - automated.
  628
+
  629
+A good way to spot untested parts of your application is to check code
  630
+coverage. This also helps identify fragile or even dead code. If you can't test
  631
+a piece of code, it usually means that code should be refactored or removed.
  632
+Coverage will help to identify dead code. See
  633
+:ref:`topics-testing-code-coverage` for details.
  634
+
  635
+:doc:`Testing Django applications </topics/testing>` has comprehensive
  636
+information about testing.
  637
+
  638
+.. _Selenium: http://seleniumhq.org/
  639
+.. _continuous integration: http://en.wikipedia.org/wiki/Continuous_integration
  640
+
  641
+What's next?
  642
+============
  643
+
  644
+The beginner tutorial ends here for the time being. In the meantime, you might
  645
+want to check out some pointers on :doc:`where to go from here
  646
+</intro/whatsnext>`.
  647
+
  648
+If you are familiar with Python packaging and interested in learning how to
  649
+turn polls into a "reusable app", check out :doc:`Advanced tutorial: How to
  650
+write reusable apps</intro/reusable-apps>`.

0 notes on commit b052e6c

Please sign in to comment.
Something went wrong with that request. Please try again.