Arthur de Jong

Open Source / Free Software developer

summaryrefslogtreecommitdiffstats
path: root/docs/intro/tutorial05.txt
blob: 617b4d077e5afedb2355bd32e41e3cc2c8e9e7b7 (plain)
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
=====================================
Writing your first Django app, part 5
=====================================

This tutorial begins where :doc:`Tutorial 4 </intro/tutorial04>` left off.
We've built a Web-poll application, and we'll now create some automated tests
for it.

Introducing automated testing
=============================

What are automated tests?
-------------------------

Tests are simple routines that check the operation of your code.

Testing operates at different levels. Some tests might apply to a tiny detail
(*does a particular model method return values as expected?*) while others
examine the overall operation of the software (*does a sequence of user inputs
on the site produce the desired result?*). That's no different from the kind of
testing you did earlier in :doc:`Tutorial 2 </intro/tutorial02>`, using the
:djadmin:`shell` to examine the behavior of a method, or running the
application and entering data to check how it behaves.

What's different in *automated* tests is that the testing work is done for
you by the system. You create a set of tests once, and then as you make changes
to your app, you can check that your code still works as you originally
intended, without having to perform time consuming manual testing.

Why you need to create tests
----------------------------

So why create tests, and why now?

You may feel that you have quite enough on your plate just learning
Python/Django, and having yet another thing to learn and do may seem
overwhelming and perhaps unnecessary. After all, our polls application is
working quite happily now; going through the trouble of creating automated
tests is not going to make it work any better. If creating the polls
application is the last bit of Django programming you will ever do, then true,
you don't need to know how to create automated tests. But, if that's not the
case, now is an excellent time to learn.

Tests will save you time
~~~~~~~~~~~~~~~~~~~~~~~~

Up to a certain point, 'checking that it seems to work' will be a satisfactory
test. In a more sophisticated application, you might have dozens of complex
interactions between components.

A change in any of those components could have unexpected consequences on the
application's behavior. Checking that it still 'seems to work' could mean
running through your code's functionality with twenty different variations of
your test data just to make sure you haven't broken something - not a good use
of your time.

That's especially true when automated tests could do this for you in seconds.
If something's gone wrong, tests will also assist in identifying the code
that's causing the unexpected behavior.

Sometimes it may seem a chore to tear yourself away from your productive,
creative programming work to face the unglamorous and unexciting business
of writing tests, particularly when you know your code is working properly.

However, the task of writing tests is a lot more fulfilling than spending hours
testing your application manually or trying to identify the cause of a
newly-introduced problem.

Tests don't just identify problems, they prevent them
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

It's a mistake to think of tests merely as a negative aspect of development.

Without tests, the purpose or intended behavior of an application might be
rather opaque. Even when it's your own code, you will sometimes find yourself
poking around in it trying to find out what exactly it's doing.

Tests change that; they light up your code from the inside, and when something
goes wrong, they focus light on the part that has gone wrong - *even if you
hadn't even realized it had gone wrong*.

Tests make your code more attractive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You might have created a brilliant piece of software, but you will find that
many other developers will simply refuse to look at it because it lacks tests;
without tests, they won't trust it. Jacob Kaplan-Moss, one of Django's
original developers, says "Code without tests is broken by design."

That other developers want to see tests in your software before they take it
seriously is yet another reason for you to start writing tests.

Tests help teams work together
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The previous points are written from the point of view of a single developer
maintaining an application. Complex applications will be maintained by teams.
Tests guarantee that colleagues don't inadvertently break your code (and that
you don't break theirs without knowing). If you want to make a living as a
Django programmer, you must be good at writing tests!

Basic testing strategies
========================

There are many ways to approach writing tests.

Some programmers follow a discipline called "`test-driven development`_"; they
actually write their tests before they write their code. This might seem
counter-intuitive, but in fact it's similar to what most people will often do
anyway: they describe a problem, then create some code to solve it. Test-driven
development simply formalizes the problem in a Python test case.

More often, a newcomer to testing will create some code and later decide that
it should have some tests. Perhaps it would have been better to write some
tests earlier, but it's never too late to get started.

Sometimes it's difficult to figure out where to get started with writing tests.
If you have written several thousand lines of Python, choosing something to
test might not be easy. In such a case, it's fruitful to write your first test
the next time you make a change, either when you add a new feature or fix a bug.

So let's do that right away.

.. _test-driven development: https://en.wikipedia.org/wiki/Test-driven_development

Writing our first test
======================

We identify a bug
-----------------

Fortunately, there's a little bug in the ``polls`` application for us to fix
right away: the ``Question.was_published_recently()`` method returns ``True`` if
the ``Question`` was published within the last day (which is correct) but also if
the ``Question``’s ``pub_date`` field is in the future (which certainly isn't).

To check if the bug really exists, using the Admin create a question whose date
lies in the future and check the method using the :djadmin:`shell`::

    >>> import datetime
    >>> from django.utils import timezone
    >>> from polls.models import Question
    >>> # create a Question instance with pub_date 30 days in the future
    >>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
    >>> # was it published recently?
    >>> future_question.was_published_recently()
    True

Since things in the future are not 'recent', this is clearly wrong.

Create a test to expose the bug
-------------------------------

What we've just done in the :djadmin:`shell` to test for the problem is exactly
what we can do in an automated test, so let's turn that into an automated test.

A conventional place for an application's tests is in the application's
``tests.py`` file; the testing system will automatically find tests in any file
whose name begins with ``test``.

Put the following in the ``tests.py`` file in the ``polls`` application:

.. snippet::
    :filename: polls/tests.py

    import datetime

    from django.utils import timezone
    from django.test import TestCase

    from .models import Question


    class QuestionMethodTests(TestCase):

        def test_was_published_recently_with_future_question(self):
            """
            was_published_recently() should return False for questions whose
            pub_date is in the future.
            """
            time = timezone.now() + datetime.timedelta(days=30)
            future_question = Question(pub_date=time)
            self.assertEqual(future_question.was_published_recently(), False)

What we have done here is created a :class:`django.test.TestCase` subclass
with a method that creates a ``Question`` instance with a ``pub_date`` in the
future. We then check the output of ``was_published_recently()`` - which
*ought* to be False.

Running tests
-------------

In the terminal, we can run our test::

    $ python manage.py test polls

and you'll see something like::

    Creating test database for alias 'default'...
    F
    ======================================================================
    FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
        self.assertEqual(future_question.was_published_recently(), False)
    AssertionError: True != False

    ----------------------------------------------------------------------
    Ran 1 test in 0.001s

    FAILED (failures=1)
    Destroying test database for alias 'default'...

What happened is this:

* ``python manage.py test polls`` looked for tests in the ``polls`` application

* it found a subclass of the :class:`django.test.TestCase` class

* it created a special database for the purpose of testing

* it looked for test methods - ones whose names begin with ``test``

* in ``test_was_published_recently_with_future_question`` it created a ``Question``
  instance whose ``pub_date`` field is 30 days in the future

* ... and using the ``assertEqual()`` method, it discovered that its
  ``was_published_recently()`` returns ``True``, though we wanted it to return
  ``False``

The test informs us which test failed and even the line on which the failure
occurred.

Fixing the bug
--------------

We already know what the problem is: ``Question.was_published_recently()`` should
return ``False`` if its ``pub_date`` is in the future. Amend the method in
``models.py``, so that it will only return ``True`` if the date is also in the
past:

.. snippet::
    :filename: polls/models.py

    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now

and run the test again::

    Creating test database for alias 'default'...
    .
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s

    OK
    Destroying test database for alias 'default'...

After identifying a bug, we wrote a test that exposes it and corrected the bug
in the code so our test passes.

Many other things might go wrong with our application in the future, but we can
be sure that we won't inadvertently reintroduce this bug, because simply
running the test will warn us immediately. We can consider this little portion
of the application pinned down safely forever.

More comprehensive tests
------------------------

While we're here, we can further pin down the ``was_published_recently()``
method; in fact, it would be positively embarrassing if in fixing one bug we had
introduced another.

Add two more test methods to the same class, to test the behavior of the method
more comprehensively:

.. snippet::
    :filename: polls/tests.py

    def test_was_published_recently_with_old_question(self):
        """
        was_published_recently() should return False for questions whose
        pub_date is older than 1 day.
        """
        time = timezone.now() - datetime.timedelta(days=30)
        old_question = Question(pub_date=time)
        self.assertEqual(old_question.was_published_recently(), False)

    def test_was_published_recently_with_recent_question(self):
        """
        was_published_recently() should return True for questions whose
        pub_date is within the last day.
        """
        time = timezone.now() - datetime.timedelta(hours=1)
        recent_question = Question(pub_date=time)
        self.assertEqual(recent_question.was_published_recently(), True)

And now we have three tests that confirm that ``Question.was_published_recently()``
returns sensible values for past, recent, and future questions.

Again, ``polls`` is a simple application, but however complex it grows in the
future and whatever other code it interacts with, we now have some guarantee
that the method we have written tests for will behave in expected ways.

Test a view
===========

The polls application is fairly undiscriminating: it will publish any question,
including ones whose ``pub_date`` field lies in the future. We should improve
this. Setting a ``pub_date`` in the future should mean that the Question is
published at that moment, but invisible until then.

A test for a view
-----------------

When we fixed the bug above, we wrote the test first and then the code to fix
it. In fact that was a simple example of test-driven development, but it
doesn't really matter in which order we do the work.

In our first test, we focused closely on the internal behavior of the code. For
this test, we want to check its behavior as it would be experienced by a user
through a web browser.

Before we try to fix anything, let's have a look at the tools at our disposal.

The Django test client
----------------------

Django provides a test :class:`~django.test.Client` to simulate a user
interacting with the code at the view level.  We can use it in ``tests.py``
or even in the :djadmin:`shell`.

We will start again with the :djadmin:`shell`, where we need to do a couple of
things that won't be necessary in ``tests.py``. The first is to set up the test
environment in the :djadmin:`shell`::

    >>> from django.test.utils import setup_test_environment
    >>> setup_test_environment()

:meth:`~django.test.utils.setup_test_environment` installs a template renderer
which will allow us to examine some additional attributes on responses such as
``response.context`` that otherwise wouldn't be available. Note that this
method *does not* setup a test database, so the following will be run against
the existing database and the output may differ slightly depending on what
questions you already created.

Next we need to import the test client class (later in ``tests.py`` we will use
the :class:`django.test.TestCase` class, which comes with its own client, so
this won't be required)::

    >>> from django.test import Client
    >>> # create an instance of the client for our use
    >>> client = Client()

With that ready, we can ask the client to do some work for us::

    >>> # get a response from '/'
    >>> response = client.get('/')
    >>> # we should expect a 404 from that address
    >>> response.status_code
    404
    >>> # on the other hand we should expect to find something at '/polls/'
    >>> # we'll use 'reverse()' rather than a hardcoded URL
    >>> from django.core.urlresolvers import reverse
    >>> response = client.get(reverse('polls:index'))
    >>> response.status_code
    200
    >>> response.content
    b'\n\n\n    <p>No polls are available.</p>\n\n'
    >>> # note - you might get unexpected results if your ``TIME_ZONE``
    >>> # in ``settings.py`` is not correct. If you need to change it,
    >>> # you will also need to restart your shell session
    >>> from polls.models import Question
    >>> from django.utils import timezone
    >>> # create a Question and save it
    >>> q = Question(question_text="Who is your favorite Beatle?", pub_date=timezone.now())
    >>> q.save()
    >>> # check the response once again
    >>> response = client.get('/polls/')
    >>> response.content
    b'\n\n\n    <ul>\n    \n        <li><a href="/polls/1/">Who is your favorite Beatle?</a></li>\n    \n    </ul>\n\n'
    >>> # If the following doesn't work, you probably omitted the call to
    >>> # setup_test_environment() described above
    >>> response.context['latest_question_list']
    <QuerySet [<Question: Who is your favorite Beatle?>]>

Improving our view
------------------

The list of polls shows polls that aren't published yet (i.e. those that have a
``pub_date`` in the future). Let's fix that.

In :doc:`Tutorial 4 </intro/tutorial04>` we introduced a class-based view,
based on :class:`~django.views.generic.list.ListView`:

.. snippet::
    :filename: polls/views.py

    class IndexView(generic.ListView):
        template_name = 'polls/index.html'
        context_object_name = 'latest_question_list'

        def get_queryset(self):
            """Return the last five published questions."""
            return Question.objects.order_by('-pub_date')[:5]

We need to amend the ``get_queryset()`` method and change it so that it also
checks the date by comparing it with ``timezone.now()``. First we need to add
an import:

.. snippet::
    :filename: polls/views.py

    from django.utils import timezone

and then we must amend the ``get_queryset`` method like so:

.. snippet::
    :filename: polls/views.py

    def get_queryset(self):
        """
        Return the last five published questions (not including those set to be
        published in the future).
        """
        return Question.objects.filter(
            pub_date__lte=timezone.now()
        ).order_by('-pub_date')[:5]

``Question.objects.filter(pub_date__lte=timezone.now())`` returns a queryset
containing ``Question``\s whose ``pub_date`` is less than or equal to - that
is, earlier than or equal to - ``timezone.now``.

Testing our new view
--------------------

Now you can satisfy yourself that this behaves as expected by firing up the
runserver, loading the site in your browser, creating ``Questions`` with dates
in the past and future, and checking that only those that have been published
are listed.  You don't want to have to do that *every single time you make any
change that might affect this* - so let's also create a test, based on our
:djadmin:`shell` session above.

Add the following to ``polls/tests.py``:

.. snippet::
    :filename: polls/tests.py

    from django.core.urlresolvers import reverse

and we'll create a shortcut function to create questions as well as a new test
class:

.. snippet::
    :filename: polls/tests.py

    def create_question(question_text, days):
        """
        Creates a question with the given `question_text` published the given
        number of `days` offset to now (negative for questions published
        in the past, positive for questions that have yet to be published).
        """
        time = timezone.now() + datetime.timedelta(days=days)
        return Question.objects.create(question_text=question_text,
                                       pub_date=time)


    class QuestionViewTests(TestCase):
        def test_index_view_with_no_questions(self):
            """
            If no questions exist, an appropriate message should be displayed.
            """
            response = self.client.get(reverse('polls:index'))
            self.assertEqual(response.status_code, 200)
            self.assertContains(response, "No polls are available.")
            self.assertQuerysetEqual(response.context['latest_question_list'], [])

        def test_index_view_with_a_past_question(self):
            """
            Questions with a pub_date in the past should be displayed on the
            index page.
            """
            create_question(question_text="Past question.", days=-30)
            response = self.client.get(reverse('polls:index'))
            self.assertQuerysetEqual(
                response.context['latest_question_list'],
                ['<Question: Past question.>']
            )

        def test_index_view_with_a_future_question(self):
            """
            Questions with a pub_date in the future should not be displayed on
            the index page.
            """
            create_question(question_text="Future question.", days=30)
            response = self.client.get(reverse('polls:index'))
            self.assertContains(response, "No polls are available.",
                                status_code=200)
            self.assertQuerysetEqual(response.context['latest_question_list'], [])

        def test_index_view_with_future_question_and_past_question(self):
            """
            Even if both past and future questions exist, only past questions
            should be displayed.
            """
            create_question(question_text="Past question.", days=-30)
            create_question(question_text="Future question.", days=30)
            response = self.client.get(reverse('polls:index'))
            self.assertQuerysetEqual(
                response.context['latest_question_list'],
                ['<Question: Past question.>']
            )

        def test_index_view_with_two_past_questions(self):
            """
            The questions index page may display multiple questions.
            """
            create_question(question_text="Past question 1.", days=-30)
            create_question(question_text="Past question 2.", days=-5)
            response = self.client.get(reverse('polls:index'))
            self.assertQuerysetEqual(
                response.context['latest_question_list'],
                ['<Question: Past question 2.>', '<Question: Past question 1.>']
            )


Let's look at some of these more closely.

First is a question shortcut function, ``create_question``, to take some
repetition out of the process of creating questions.

``test_index_view_with_no_questions`` doesn't create any questions, but checks
the message: "No polls are available." and verifies the ``latest_question_list``
is empty. Note that the :class:`django.test.TestCase` class provides some
additional assertion methods. In these examples, we use
:meth:`~django.test.SimpleTestCase.assertContains()` and
:meth:`~django.test.TransactionTestCase.assertQuerysetEqual()`.

In ``test_index_view_with_a_past_question``, we create a question and verify that it
appears in the list.

In ``test_index_view_with_a_future_question``, we create a question with a
``pub_date`` in the future. The database is reset for each test method, so the
first question is no longer there, and so again the index shouldn't have any
questions in it.

And so on. In effect, we are using the tests to tell a story of admin input
and user experience on the site, and checking that at every state and for every
new change in the state of the system, the expected results are published.

Testing the ``DetailView``
--------------------------

What we have works well; however, even though future questions don't appear in
the *index*, users can still reach them if they know or guess the right URL. So
we need to add a similar  constraint to ``DetailView``:

.. snippet::
    :filename: polls/views.py

    class DetailView(generic.DetailView):
        ...
        def get_queryset(self):
            """
            Excludes any questions that aren't published yet.
            """
            return Question.objects.filter(pub_date__lte=timezone.now())

And of course, we will add some tests, to check that a ``Question`` whose
``pub_date`` is in the past can be displayed, and that one with a ``pub_date``
in the future is not:

.. snippet::
    :filename: polls/tests.py

    class QuestionIndexDetailTests(TestCase):
        def test_detail_view_with_a_future_question(self):
            """
            The detail view of a question with a pub_date in the future should
            return a 404 not found.
            """
            future_question = create_question(question_text='Future question.',
                                              days=5)
            response = self.client.get(reverse('polls:detail',
                                       args=(future_question.id,)))
            self.assertEqual(response.status_code, 404)

        def test_detail_view_with_a_past_question(self):
            """
            The detail view of a question with a pub_date in the past should
            display the question's text.
            """
            past_question = create_question(question_text='Past Question.',
                                            days=-5)
            response = self.client.get(reverse('polls:detail',
                                       args=(past_question.id,)))
            self.assertContains(response, past_question.question_text,
                                status_code=200)


Ideas for more tests
--------------------

We ought to add a similar ``get_queryset`` method to ``ResultsView`` and
create a new test class for that view. It'll be very similar to what we have
just created; in fact there will be a lot of repetition.

We could also improve our application in other ways, adding tests along the
way. For example, it's silly that ``Questions`` can be published on the site
that have no ``Choices``. So, our views could check for this, and exclude such
``Questions``. Our tests would create a ``Question`` without ``Choices`` and
then test that it's not published, as well as create a similar ``Question``
*with* ``Choices``, and test that it *is* published.

Perhaps logged-in admin users should be allowed to see unpublished
``Questions``, but not ordinary visitors. Again: whatever needs to be added to
the software to accomplish this should be accompanied by a test, whether you
write the test first and then make the code pass the test, or work out the
logic in your code first and then write a test to prove it.

At a certain point you are bound to look at your tests and wonder whether your
code is suffering from test bloat, which brings us to:

When testing, more is better
============================

It might seem that our tests are growing out of control. At this rate there will
soon be more code in our tests than in our application, and the repetition
is unaesthetic, compared to the elegant conciseness of the rest of our code.

**It doesn't matter**. Let them grow. For the most part, you can write a test
once and then forget about it. It will continue performing its useful function
as you continue to develop your program.

Sometimes tests will need to be updated. Suppose that we amend our views so that
only ``Questions`` with ``Choices`` are published. In that case, many of our
existing tests will fail - *telling us exactly which tests need to be amended to
bring them up to date*, so to that extent tests help look after themselves.

At worst, as you continue developing, you might find that you have some tests
that are now redundant. Even that's not a problem; in testing redundancy is
a *good* thing.

As long as your tests are sensibly arranged, they won't become unmanageable.
Good rules-of-thumb include having:

* a separate ``TestClass`` for each model or view
* a separate test method for each set of conditions you want to test
* test method names that describe their function

Further testing
===============

This tutorial only introduces some of the basics of testing. There's a great
deal more you can do, and a number of very useful tools at your disposal to
achieve some very clever things.

For example, while our tests here have covered some of the internal logic of a
model and the way our views publish information, you can use an "in-browser"
framework such as Selenium_ to test the way your HTML actually renders in a
browser. These tools allow you to check not just the behavior of your Django
code, but also, for example, of your JavaScript. It's quite something to see
the tests launch a browser, and start interacting with your site, as if a human
being were driving it! Django includes :class:`~django.test.LiveServerTestCase`
to facilitate integration with tools like Selenium.

If you have a complex application, you may want to run tests automatically
with every commit for the purposes of `continuous integration`_, so that
quality control is itself - at least partially - automated.

A good way to spot untested parts of your application is to check code
coverage. This also helps identify fragile or even dead code. If you can't test
a piece of code, it usually means that code should be refactored or removed.
Coverage will help to identify dead code. See
:ref:`topics-testing-code-coverage` for details.

:doc:`Testing in Django </topics/testing/index>` has comprehensive
information about testing.

.. _Selenium: http://seleniumhq.org/
.. _continuous integration: https://en.wikipedia.org/wiki/Continuous_integration

What's next?
============

For full details on testing, see :doc:`Testing in Django
</topics/testing/index>`.

When you're comfortable with testing Django views, read
:doc:`part 6 of this tutorial</intro/tutorial06>` to learn about
static files management.