Ticket #9962: 9962.diff

File 9962.diff, 29.3 KB (added by Tim Graham, 8 years ago)
  • docs/index.txt

    diff --git a/docs/index.txt b/docs/index.txt
    index a6d9ed2..acbb12f 100644
    a b Are you new to Django or to programming? This is the place to start! 
    4444  :doc:`Part 1 <intro/tutorial01>` |
    4545  :doc:`Part 2 <intro/tutorial02>` |
    4646  :doc:`Part 3 <intro/tutorial03>` |
    47   :doc:`Part 4 <intro/tutorial04>`
     47  :doc:`Part 4 <intro/tutorial04>` |
     48  :doc:`Part 5 <intro/tutorial05>`
    4950* **Advanced Tutorials:**
    5051  :doc:`How to write reusable apps <intro/reusable-apps>`
  • docs/intro/index.txt

    diff --git a/docs/intro/index.txt b/docs/intro/index.txt
    index afb1825..2bf6e99 100644
    a b place: read this material to quickly get up and running. 
    1313   tutorial02
    1414   tutorial03
    1515   tutorial04
     16   tutorial05
    1617   reusable-apps
    1718   whatsnext
  • docs/intro/tutorial04.txt

    diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt
    index dfee827..1619b59 100644
    a b Run the server, and use your new polling app based on generic views. 
    275275For full details on generic views, see the :doc:`generic views documentation
    278 What's next?
    279 ============
    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>`.
    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>`.
     278When you're comfortable with forms and generic views, read :doc:`part 5 of this
     279tutorial</intro/tutorial05>` to learn about testing our polls app.
  • new file docs/intro/tutorial05.txt

    diff --git a/docs/intro/tutorial05.txt b/docs/intro/tutorial05.txt
    new file mode 100644
    index 0000000..fc6d871
    - +  
     2Writing your first Django app, part 5
     5This tutorial begins where :doc:`Tutorial 4 </intro/tutorial04>` left off.
     6We've built a Web-poll application, and we'll now create some automated tests
     7for it.
     9Introducing automated testing
     12What are automated tests?
     15Tests are simple routines that check the operation of your code.
     17Testing operates at different levels. Some tests might apply to a tiny detail
     18- *does a particular model method return values as expected?*, while others
     19examine the overall operation of the software - *does a sequence of user inputs
     20on the site produce the desired result?* That's no different from the kind of
     21testing you did earlier in :doc:`Tutorial 1 </intro/tutorial01>`, using the
     22shell to examine the behavior of a method, or running the application and
     23entering data to check how it behaves.
     25What's different in *automated* tests is that the testing work is done for
     26you by the system. You create a set of tests once, and then as you make changes
     27to your app, you can check that your code still works as you originally
     28intended, without having to perform time consuming manual testing.
     30Why you need to create tests
     33So why create tests, and why now?
     35You may feel that you have quite enough on your plate just learning
     36Python/Django, and having yet another thing to learn and do may seem
     37overwhelming and perhaps unnecessary. After all, our polls application is
     38working quite happily now; going through the trouble of creating automated
     39tests is not going to make it work any better. If creating the polls
     40application is the last bit of Django programming you will ever do, then true,
     41you don't need to know how to create automated tests. But, if that's not the
     42case, now is an excellent time to learn.
     44Tests will save you time
     47Up to a certain point, 'checking that it seems to work' will be a satisfactory
     48test. In a more sophisticated application, you might have dozens of complex
     49interactions between components.
     51A change in any of those components could have unexpected consequences on the
     52application's behavior. Checking that it still 'seems to work' could mean
     53running through your code's functionality with twenty different variations of
     54your test data just to make sure you haven't broken something - not a good use
     55of your time.
     57That's especially true when automated tests could do this for you in seconds.
     58If something's gone wrong, tests will also assist in identifying the code
     59that's causing the unexpected behavior.
     61Sometimes it may seem a chore to tear yourself away from your productive,
     62creative programming work to face the unglamorous and unexciting business
     63of writing tests, particularly when you know your code is working properly.
     65However, the task of writing tests is a lot more fulfilling than spending hours
     66testing your application manually or trying to identify the cause of a
     67newly-introduced problem.
     69Tests don't just identify problems, they prevent them
     72It's a mistake to think of tests merely as a negative aspect of development.
     74Without tests, the purpose or intended behavior of an application might be
     75rather opaque. Even when it's your own code, you will sometimes find yourself
     76poking around in it trying to find out what exactly it's doing.
     78Tests change that; they light up your code from the inside, and when something
     79goes wrong, they focus light on the part that has gone wrong - *even if you
     80hadn't even realized it had gone wrong*.
     82Tests make your code more attractive
     85You might have created a brilliant piece of software, but you will find that
     86many other developers will simply refuse to look at it because it lacks tests;
     87without tests, they won't trust it. Jacob Kaplan-Moss, one of Django's
     88original developers, says "Code without tests is broken by design."
     90That other developers want to see tests in your software before they take it
     91seriously is yet another reason for you to start writing tests.
     93Basic testing strategies
     96There are many ways to approach writing tests.
     98Some programmers follow a discipline called "`test-driven development`_"; they
     99actually write their tests before they write their code. This might seem
     100counter-intuitive, but in fact it's similar to what most people will often do
     101anyway: they describe a problem, then create some code to solve it. Test-driven
     102development simply formalizes the problem in a Python test case.
     104More often, a newcomer to testing will create some code and later decide that
     105it should have some tests. Perhaps it would have been better to write some
     106tests earlier, but it's never too late to get started.
     108Sometimes it's difficult to figure out where to get started with writing tests.
     109If you have written several thousand lines of Python, choosing something to
     110test might not be easy. In such a case, it's fruitful to write your first test
     111the next time you make a change, either when you add a new feature or fix a bug.
     113So let's do that right away.
     115.. _test-driven development: http://en.wikipedia.org/wiki/Test-driven_development/
     117Writing our first test
     120We identify a bug
     123Fortunately, there's a little bug in the ``polls`` application for us to fix
     124right away: the ``Poll.was_published_recently()`` method returns ``True`` if
     125the ``Poll`` was published within the last day (which is correct) but also if
     126the ``Poll``'s ``pub_date`` field is in the future (which certainly isn't).
     128You can see this in the Admin; create a Poll whose date lies in the future;
     129you'll see that the ``Poll`` change list claims it was published recently.
     131You can also see this using the shell::
     133    >>> import datetime
     134    >>> from django.utils import timezone
     135    >>> from polls.models import Poll
     136    >>> # create a Poll instance with pub_date 30 days in the future
     137    >>> future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
     138    >>> # was it published recently?
     139    >>> future_poll.was_published_recently()
     140    True
     142Since things in the future are not 'recent', this is clearly wrong.
     144Create a test to expose the bug
     147What we've just done in the shell to test for the problem is exactly what we
     148can do in an automated test, so let's turn that into an automated test.
     150The best place for an application's tests is in the application's ``tests.py``
     151file - the testing system will look there for tests automatically.
     153Put the following in the ``tests.py`` file in the ``polls`` application (you'll
     154notice  ``tests.py`` contains some dummy tests, you can remove those)::
     156    import datetime
     158    from django.utils import timezone
     159    from django.test import TestCase
     161    from polls.models import Poll
     163    # we'll put all the Poll method tests in a class together
     164    class PollMethodTests(TestCase):
     166        def test_was_published_recently_with_future_poll(self):
     167            # create a Poll instance whose pub_date is in the future
     168            future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
     169            # was_published_recently() should return False
     170            self.assertEqual(future_poll.was_published_recently(), False)
     172What we have done here is created a :class:`django.test.TestCase` subclass
     173with a method that creates a ``Poll`` instance with a ``pub_date`` in the
     174future. We then check the output of ``was_published_recently()`` - which
     175*ought* to be False.
     177Running tests
     180In the terminal, we can run our test::
     182    python manage.py test polls
     184and you'll see something like::
     186    Creating test database for alias 'default'...
     187    F
     188    ======================================================================
     189    FAIL: test_was_published_recently_with_future_poll (polls.tests.PollMethodTests)
     190    ----------------------------------------------------------------------
     191    Traceback (most recent call last):
     192      File "/home/daniele/django-testing-tutorial/mysite/polls/tests.py", line 18, in test_was_published_recently_with_future_poll
     193        self.assertEqual(future_poll.was_published_recently(), False)
     194    AssertionError: True != False
     196    ----------------------------------------------------------------------
     197    Ran 1 test in 0.000s
     199    FAILED (failures=1)
     200    Destroying test database for alias 'default'...
     202What happened is this:
     204* ``python manage.py test polls`` looked for tests in the ``polls`` application
     206* it found a subclass of the :class:`django.test.TestCase` class
     208* it created a special database for the purpose of testing
     210* it looked for test methods - ones whose names begin with ``test``
     212* in ``test_was_published_recently_with_future_poll`` it created a ``Poll``
     213  instance whose ``pub_date`` field is 30 days in the future
     215* ... and using the ``assertEqual()`` method, it discovered that its
     216  ``was_published_recently()`` returns ``True``, though we wanted it to return
     217  `False``
     219The test informs us which test failed -
     220``test_was_published_recently_with_future_poll`` - and even the line on which
     221the failure occurred.
     223Fixing the bug
     226We already know what the problem is: ``Poll.was_published_recently()`` should
     227return ``False`` if its ``pub_date`` is in the future. Amend the method in
     228``models.py``, so that it will only return ``True`` if the date is also in the
     231    def was_published_recently(self):
     232        return self.pub_date >= timezone.now() - datetime.timedelta(days=1) and \
     233          self.pub_date < timezone.now()
     235and run the test again::
     237    Creating test database for alias 'default'...
     238    .
     239    ----------------------------------------------------------------------
     240    Ran 1 test in 0.000s
     242    OK
     243    Destroying test database for alias 'default'...
     245Having identified a bug, we have written a test that exposes it, and corrected
     246the bug in the code.
     248Many other things might go wrong with our application in the future, but we can
     249be sure that we won't inadvertently reintroduce this bug, because simply
     250running the test will warn us immediately. We can consider this little portion
     251of the application pinned down safely forever.
     253More comprehensive tests
     256While we're here, we can further pin down the ``was_published_recently()``
     257method; in fact, it would be positively embarrassing if in fixing one bug we had
     258introduced another.
     260Add two more test methods to the same class, to test the behavior of the method
     261more comprehensively::
     263    def test_was_published_recently_with_old_poll(self):
     264        # create a Poll instance whose pub_date is 30 days ago
     265        old_poll = Poll(pub_date=timezone.now() - datetime.timedelta(days=30))
     266        # was_published_recently() should return False
     267        self.assertEqual(old_poll.was_published_recently(), False)
     269    def test_was_published_recently_with_recent_poll(self):
     270        # create a Poll instance whose pub_date is one hour ago
     271        recent_poll = Poll(pub_date=timezone.now() - datetime.timedelta(hours=1))
     272        # was_published_recently() should return True
     273        self.assertEqual(recent_poll.was_published_recently(), True)
     275And now we have three tests, that confirm that ``Poll.was_published_recently()``
     276returns sensible values for past, recent, and future Polls.
     278Again, ``polls`` is a simple application, but however complex it grows in the
     279future and whatever other code it interacts with, we now have some guarantee
     280that the method we have written tests for will behave in expected ways.
     282Test a view
     285The polls application is fairly undiscriminating: it will publish any Poll,
     286including ones whose ``pub_date`` field lies in the future.
     288We should improve this. Setting a ``pub_date`` in the future should mean that
     289the Poll is published at that moment, but invisible until then.
     291A test for a view
     294When we fixed the bug above, we wrote the test first and then the code to fix
     295it. In fact that was a simple example of test-driven development, but it
     296doesn't really matter in which order we do the work.
     298In our first test, we focused closely on the internal behavior of the code. For
     299this test, we want to check its behavior as it would be experienced by a user
     300through a web browser.
     302Before we try to fix anything, let's have a look at the tools at our disposal.
     304The Django test client
     307Django provides a test :class:`~django.test.client.Client` to simulate a user
     308interacting with the code at the view level.  We can use it in ``tests.py``
     309or even in the shell.
     311We will start again with the shell, where we need to do a couple of things that
     312won't be necessary in ``tests.py``. The first is to set up the test environment
     313in the shell::
     315    >>> from django.test.utils import setup_test_environment
     316    >>> setup_test_environment()
     318Next we need to import the test client class (later in ``tests.py`` we will use
     319the :class:`django.test.TestCase` class, which comes with its own client, so
     320this won't be required)::
     322    >>> from django.test.client import Client
     323    >>> # create an instance of the client for our use
     324    >>> client = Client()
     326With that ready, we can ask the client to do some work for us::
     328    >>> # get a response from '/'
     329    >>> response = client.get('/')
     330    >>> # we should expect a 404 from that address
     331    >>> response.status_code
     332    404
     333    >>> # on the other hand we should expect to find something at '/polls/'
     334    >>> # we'll use 'reverse()' rather than a harcoded URL
     335    >>> from django.core.urlresolvers import reverse
     336    >>> response = client.get(reverse('polls:index'))
     337    >>> response.status_code
     338    200
     339    >>> response.content
     340    '\n\n\n    <p>No polls are available.</p>\n\n'
     341    >>> # note - you might get unexpected results if your ``TIME_ZONE``
     342    >>> # in ``settings.py`` is not correct. If you need to change it,
     343    >>> # you will also need to restart your shell session
     344    >>> from polls.models import Poll
     345    >>> from django.utils import timezone
     346    >>> # create a Poll and save it
     347    >>> p = Poll(question="Who is your favourite Beatle?", pub_date=timezone.now())
     348    >>> p.save()
     349    >>> # check the response once again
     350    >>> response = client.get('/polls/')
     351    >>> response.content
     352    '\n\n\n    <ul>\n    \n        <li><a href="/polls/1/">Who is your favourite Beatle?</a></li>\n    \n    </ul>\n\n'
     353    >>> response.context['latest_poll_list']
     354    [<Poll: Who is your favourite Beatle?>]
     356Improving our view
     359The list of polls shows polls that aren't published yet (i.e. those that have a
     360``pub_date`` in the future). Let's fix that.
     362In :doc:`Tutorial 4 </intro/tutorial04>` we deleted the view functions from
     363``views.py`` in favor of a :class:`~django.views.generic.list.ListView` in
     366    url(r'^$',
     367        ListView.as_view(
     368            queryset=Poll.objects.order_by('-pub_date')[:5],
     369            context_object_name='latest_poll_list',
     370            template_name='polls/index.html'),
     371        name='index'),
     373``response.context_data['latest_poll_list']`` extracts the data this view
     374places into the context.
     376We need to amend the line that gives us the ``queryset``::
     378    queryset=Poll.objects.order_by('-pub_date')[:5],
     380Let's change the queryset so that it also checks the date by comparing it with
     381``timezone.now()``. First we need to add an import::
     383    from django.utils import timezone
     385and then we must amend the existing url function::
     387    url(r'^$',
     388        ListView.as_view(
     389            queryset=Poll.objects.order_by('-pub_date')[:5],
     390            context_object_name='latest_poll_list',
     391            template_name='polls/index.html'),
     392        name='index'),
     396    url(r'^$',
     397        ListView.as_view(
     398            queryset=Poll.objects.filter(pub_date__lte=timezone.now()) \
     399                .order_by('-pub_date')[:5],
     400            context_object_name='latest_poll_list',
     401            template_name='polls/index.html'),
     402        name='index'),
     404``Poll.objects.filter(pub_date__lte=timezone.now())`` returns a queryset
     405containing Polls whose ``pub_date`` is less than or equal to - that is, earlier
     406than or equal to - ``timezone.now()``.
     408Testing our new view
     411Now you can satisfy yourself that this behaves as expected by firing up the
     412runserver, loading the site in your browser, creating ``Polls`` with dates in
     413the past and future, and checking that only those that have been published are
     414listed.  You don't want to have to do that *every single time you make any
     415change that might affect this* - so let's also create a test, based on our
     416shell session above.
     418Add the following to ``polls/tests.py``::
     420    # to work out URLs from the view we need urlresolvers.reverse
     421    from django.core.urlresolvers import reverse
     423and we'll create a new class too::
     425    # we'll put all the view tests in a class together
     426    class PollViewTests(TestCase):
     427        # setUp will set up our testing environment for each and every test method
     428        # in the class
     429        def setUp(self):
     430            # We'll set up some objects that we'll over and over again in our tests,
     431            # so that we don't have to repeat ourselves. Note that we don't save
     432            # them until we need them in the database - we do that in the test
     433            # methods themselves.
     435            # a Poll instance whose pub_date is 30 days ago
     436            self.beatles_poll = Poll(
     437                question="Who is your favourite Beatle?",
     438                pub_date=timezone.now() - datetime.timedelta(days=30)
     439            )
     441            # a Poll instance whose pub_date is 30 in the future
     442            self.stones_poll = Poll(
     443                question="Who is your favourite Rolling Stone?",
     444                pub_date=timezone.now() + datetime.timedelta(days=30)
     445            )
     447        def test_index_view_with_no_polls(self):
     448            # issue a GET request.
     449            response = self.client.get(reverse('polls:index'))
     450            # we should get a 200 OK
     451            self.assertEqual(response.status_code, 200)
     452            # the response should contain a message
     453            self.assertContains(response, "No polls are available.")
     454            # there should be nothing in the latest_poll_list
     455            self.assertEqual(len(response.context['latest_poll_list']), 0)
     457        def test_index_view_with_a_past_poll(self):
     458            self.beatles_poll.save()
     459            response = self.client.get(reverse('polls:index'))
     460            # the only item in the list should be beatles_poll
     461            self.assertEqual(len(response.context['latest_poll_list']), 1)
     462            self.assertEqual(response.context['latest_poll_list'][0], self.beatles_poll)
     464        def test_index_view_with_a_future_poll(self):
     465            self.stones_poll.save()
     466            response = self.client.get(reverse('polls:index'))
     467            # there should be no polls in the index
     468            # assertContains() allows us to check a number of things in one go
     469            self.assertContains(response, "No polls are available.", status_code=200)
     470            self.assertEqual(len(response.context['latest_poll_list']), 0)
     472        def test_index_view_with_future_poll_and_past_poll(self):
     473            self.beatles_poll.save()
     474            self.stones_poll.save()
     475            response = self.client.get(reverse('polls:index'))
     476            # the only item in the list should *still* be beatles_poll
     477            self.assertEqual(len(response.context['latest_poll_list']), 1)
     478            self.assertEqual(response.context['latest_poll_list'][0], self.beatles_poll)
     480        def test_index_view_with_two_past_polls(self):
     481            self.beatles_poll.save()
     482            # change the date on the stones_poll - put it in the past
     483            self.stones_poll.pub_date = timezone.now() - datetime.timedelta(days=5)
     484            self.stones_poll.save()
     485            response = self.client.get(reverse('polls:index'))
     486            # we should have 2 items now, stones_poll first and beatles_poll second
     487            self.assertEqual(len(response.context['latest_poll_list']), 2)
     488            self.assertEqual(response.context['latest_poll_list'][0], self.stones_poll)
     489            self.assertEqual(response.context['latest_poll_list'][1], self.beatles_poll)
     491Let's look at some of these more closely.
     493All the test methods run ``setUp()``, which creates the two ``Poll`` instances.
     494It doesn't save them though; we only call ``save()`` when we need them.
     496``test_index_view_with_no_polls`` hasn't saved either of the ``Polls``, so we
     497expect the poll index view load a page, but to return the message: "No polls are
     498available.", and the ``latest_poll_list`` variable to be empty.
     500In ``test_index_view_with_a_past_poll``, a poll has been saved to the database,
     501so we *do* expect to find it in the list.
     503In ``test_index_view_with_a_future_poll``, we save the other poll. The database
     504is reset for each test method, so the first poll is no longer there, and so
     505again the index shouldn't have any polls in it. Note that there is more than
     506one ``assert`` method at our disposal; here we are using a new one,
     509And so on. In effect, we are using the tests to tell a story of admin input
     510and user experience on the site, and checking that at every state and for every
     511new change in the state of the system, the expected results are published.
     513Testing the ``DetailView``
     516What we have works well; however, even though future polls don't appear in the
     517*index*, users can still reach them if they know or guess the right URL. So we
     518need similar constraints in the ``DetailViews``, by adding::
     520    queryset=Poll.objects.filter(pub_date__lte=timezone.now())
     522to them - for example::
     524    url(r'^(?P<pk>\d+)/$',
     525        DetailView.as_view(
     526            queryset=Poll.objects.filter(pub_date__lte=timezone.now()),
     527            model=Poll,
     528            template_name='polls/detail.html'),
     529        name='detail'),
     531and of course, we will add some tests, to check that a ``Poll`` whose
     532``pub_date`` is in the past can be displayed, and that one with a ``pub_date``
     533in the future is not::
     535    class PollIndexDetailTests(TestCase):
     536        def setUp(self):
     537            # a Poll instance whose pub_date is 30 in the future
     538            self.stones_poll = Poll(
     539                question="Who is your favourite Rolling Stone?",
     540                pub_date=timezone.now() + datetime.timedelta(days=30)
     541            )
     543        def test_detail_view_with_a_future_poll(self):
     544            self.stones_poll.save()
     545            response = self.client.get(reverse('polls:detail', args=(self.stones_poll.id,)))
     546            # should return a 404 not found
     547            self.assertEqual(response.status_code, 404)
     549        def test_detail_view_with_a_past_poll(self):
     550            # change the date on the stones_poll - put it in the past
     551            self.stones_poll.pub_date = timezone.now() - datetime.timedelta(days=5)
     552            self.stones_poll.save()
     553            response = self.client.get(reverse('polls:detail', args=(self.stones_poll.id,)))
     554            # now the same poll should return a 200 OK
     555            self.assertContains(response, self.stones_poll.question, status_code=200)
     557Ideas for more tests
     560We ought to add similar ``queryset`` arguments to the other DetailView URLs,
     561and create a new test class for each view. They'll be very similar to what we
     562have just created; in fact there will be a lot of repetition.
     564We could also improve our application in other ways, adding tests along the
     565way. For example, it's silly that ``Polls`` can be published on the site that
     566have no ``Choices``. So, our views could check for this, and exclude such
     567``Polls``. Our tests would create a ``Poll`` without ``Choices`` and then test
     568that it's not published, as well as create a similar ``Poll`` *with*
     569``Choices``, and test that it *is* published.
     571Perhaps logged-in admin users should be allowed to see unpublished ``Polls``,
     572but not ordinary visitors. Again: whatever needs to be added to the software to
     573accomplish this should be accompanied by a test, whether you write the test
     574first and then make the code pass the test, or work out the logic in your code
     575first and then write a test to prove it.
     577At a certain point you are bound to look at your tests and wonder whether your
     578code is suffering from test bloat, which brings us to:
     580When testing, more is better
     583It might seem that our tests are growing out of control. At this rate there will
     584soon be more code in our tests than in our application, and the repetition
     585is unaesthetic, compared to the elegant conciseness of the rest of our code.
     587**It doesn't matter**. Let them grow. For the most part, you can write a test
     588once and then forget about it. It will continue performing its useful function
     589as you continue to develop your program.
     591Sometimes tests will need to be updated. Suppose that we amend our views so that
     592only ``Polls`` with ``Choices`` are published. In that case, many of our
     593existing tests will fail - *telling us exactly which tests need to be amended to
     594bring them up to date*, so to that extent tests help look after themselves.
     596At worst, as you continue developing, you might find that you have some tests
     597that are now redundant. Even that's not a problem; in testing redundancy is
     598a *good* thing.
     600As long as your tests are sensibly arranged, they won't become unmanageable.
     601Good rules-of-thumb include having:
     603* a separate ``TestClass`` for each model or view
     604* a separate test method for each set of conditions you want to test
     605* test method names that describe their function
     607Further testing
     610This tutorial only introduces some of the basics of testing. There's a great
     611deal more you can do, and a number of very useful tools at your disposal to
     612achieve some very clever things.
     614For example, while our tests here have covered some of the internal logic of a
     615model and the way our views publish information, you can use an "in-browser"
     616framework such as Selenium_ to test the way your HTML actually renders in a
     617browser. These tools allow you to check not just the behavior of your Django
     618code, but also, for example, of your JavaScript. It's quite something to see
     619the tests launch a browser, and start interacting with your site, as if a human
     620being were driving it! Django includes :class:`~django.test.LiveServerTestCase`
     621to facilitate integration with tools like Selenium.
     623If you have a complex application, you may want to run tests automatically
     624with every commit for the purposes of `continuous integration`_, so that
     625quality control is itself - at least partially - automated.
     627:doc:`Testing Django applications </topics/testing>` has comprehensive
     628information about testing.
     630.. _Selenium: http://seleniumhq.org/
     631.. _continuous integration: http://en.wikipedia.org/wiki/Continuous_integration
     633What's next?
     636The beginner tutorial ends here for the time being. In the meantime, you might
     637want to check out some pointers on :doc:`where to go from here
     640If you are familiar with Python packaging and interested in learning how to
     641turn polls into a "reusable app", check out :doc:`Advanced tutorial: How to
     642write reusable apps</intro/reusable-apps>`.
Back to Top