"Fossies" - the Fresh Open Source Software Archive

Member "Django-1.11.25/docs/intro/tutorial05.txt" (1 Oct 2019, 27696 Bytes) of package /linux/www/Django-1.11.25.tar.gz:


As a special service "Fossies" has tried to format the requested text file into HTML format (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "tutorial05.txt": 2.2.5_vs_2.2.6.

    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 2 </intro/tutorial02>`, using the
   22 :djadmin:`shell` to examine the behavior of a method, or running the
   23 application and 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: https://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 ``Question.was_published_recently()`` method returns ``True`` if
  134 the ``Question`` was published within the last day (which is correct) but also if
  135 the ``Question``’s ``pub_date`` field is in the future (which certainly isn't).
  136 
  137 To check if the bug really exists, using the Admin create a question whose date
  138 lies in the future and check the method using the :djadmin:`shell`::
  139 
  140     >>> import datetime
  141     >>> from django.utils import timezone
  142     >>> from polls.models import Question
  143     >>> # create a Question instance with pub_date 30 days in the future
  144     >>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
  145     >>> # was it published recently?
  146     >>> future_question.was_published_recently()
  147     True
  148 
  149 Since things in the future are not 'recent', this is clearly wrong.
  150 
  151 Create a test to expose the bug
  152 -------------------------------
  153 
  154 What we've just done in the :djadmin:`shell` to test for the problem is exactly
  155 what we can do in an automated test, so let's turn that into an automated test.
  156 
  157 A conventional place for an application's tests is in the application's
  158 ``tests.py`` file; the testing system will automatically find tests in any file
  159 whose name begins with ``test``.
  160 
  161 Put the following in the ``tests.py`` file in the ``polls`` application:
  162 
  163 .. snippet::
  164     :filename: polls/tests.py
  165 
  166     import datetime
  167 
  168     from django.utils import timezone
  169     from django.test import TestCase
  170 
  171     from .models import Question
  172 
  173 
  174     class QuestionModelTests(TestCase):
  175 
  176         def test_was_published_recently_with_future_question(self):
  177             """
  178             was_published_recently() returns False for questions whose pub_date
  179             is in the future.
  180             """
  181             time = timezone.now() + datetime.timedelta(days=30)
  182             future_question = Question(pub_date=time)
  183             self.assertIs(future_question.was_published_recently(), False)
  184 
  185 What we have done here is created a :class:`django.test.TestCase` subclass
  186 with a method that creates a ``Question`` instance with a ``pub_date`` in the
  187 future. We then check the output of ``was_published_recently()`` - which
  188 *ought* to be False.
  189 
  190 Running tests
  191 -------------
  192 
  193 In the terminal, we can run our test::
  194 
  195     $ python manage.py test polls
  196 
  197 and you'll see something like::
  198 
  199     Creating test database for alias 'default'...
  200     System check identified no issues (0 silenced).
  201     F
  202     ======================================================================
  203     FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
  204     ----------------------------------------------------------------------
  205     Traceback (most recent call last):
  206       File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
  207         self.assertIs(future_question.was_published_recently(), False)
  208     AssertionError: True is not False
  209 
  210     ----------------------------------------------------------------------
  211     Ran 1 test in 0.001s
  212 
  213     FAILED (failures=1)
  214     Destroying test database for alias 'default'...
  215 
  216 What happened is this:
  217 
  218 * ``python manage.py test polls`` looked for tests in the ``polls`` application
  219 
  220 * it found a subclass of the :class:`django.test.TestCase` class
  221 
  222 * it created a special database for the purpose of testing
  223 
  224 * it looked for test methods - ones whose names begin with ``test``
  225 
  226 * in ``test_was_published_recently_with_future_question`` it created a ``Question``
  227   instance whose ``pub_date`` field is 30 days in the future
  228 
  229 * ... and using the ``assertIs()`` method, it discovered that its
  230   ``was_published_recently()`` returns ``True``, though we wanted it to return
  231   ``False``
  232 
  233 The test informs us which test failed and even the line on which the failure
  234 occurred.
  235 
  236 Fixing the bug
  237 --------------
  238 
  239 We already know what the problem is: ``Question.was_published_recently()`` should
  240 return ``False`` if its ``pub_date`` is in the future. Amend the method in
  241 ``models.py``, so that it will only return ``True`` if the date is also in the
  242 past:
  243 
  244 .. snippet::
  245     :filename: polls/models.py
  246 
  247     def was_published_recently(self):
  248         now = timezone.now()
  249         return now - datetime.timedelta(days=1) <= self.pub_date <= now
  250 
  251 and run the test again::
  252 
  253     Creating test database for alias 'default'...
  254     System check identified no issues (0 silenced).
  255     .
  256     ----------------------------------------------------------------------
  257     Ran 1 test in 0.001s
  258 
  259     OK
  260     Destroying test database for alias 'default'...
  261 
  262 After identifying a bug, we wrote a test that exposes it and corrected the bug
  263 in the code so our test passes.
  264 
  265 Many other things might go wrong with our application in the future, but we can
  266 be sure that we won't inadvertently reintroduce this bug, because simply
  267 running the test will warn us immediately. We can consider this little portion
  268 of the application pinned down safely forever.
  269 
  270 More comprehensive tests
  271 ------------------------
  272 
  273 While we're here, we can further pin down the ``was_published_recently()``
  274 method; in fact, it would be positively embarrassing if in fixing one bug we had
  275 introduced another.
  276 
  277 Add two more test methods to the same class, to test the behavior of the method
  278 more comprehensively:
  279 
  280 .. snippet::
  281     :filename: polls/tests.py
  282 
  283     def test_was_published_recently_with_old_question(self):
  284         """
  285         was_published_recently() returns False for questions whose pub_date
  286         is older than 1 day.
  287         """
  288         time = timezone.now() - datetime.timedelta(days=1, seconds=1)
  289         old_question = Question(pub_date=time)
  290         self.assertIs(old_question.was_published_recently(), False)
  291 
  292     def test_was_published_recently_with_recent_question(self):
  293         """
  294         was_published_recently() returns True for questions whose pub_date
  295         is within the last day.
  296         """
  297         time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
  298         recent_question = Question(pub_date=time)
  299         self.assertIs(recent_question.was_published_recently(), True)
  300 
  301 And now we have three tests that confirm that ``Question.was_published_recently()``
  302 returns sensible values for past, recent, and future questions.
  303 
  304 Again, ``polls`` is a simple application, but however complex it grows in the
  305 future and whatever other code it interacts with, we now have some guarantee
  306 that the method we have written tests for will behave in expected ways.
  307 
  308 Test a view
  309 ===========
  310 
  311 The polls application is fairly undiscriminating: it will publish any question,
  312 including ones whose ``pub_date`` field lies in the future. We should improve
  313 this. Setting a ``pub_date`` in the future should mean that the Question is
  314 published at that moment, but invisible until then.
  315 
  316 A test for a view
  317 -----------------
  318 
  319 When we fixed the bug above, we wrote the test first and then the code to fix
  320 it. In fact that was a simple example of test-driven development, but it
  321 doesn't really matter in which order we do the work.
  322 
  323 In our first test, we focused closely on the internal behavior of the code. For
  324 this test, we want to check its behavior as it would be experienced by a user
  325 through a web browser.
  326 
  327 Before we try to fix anything, let's have a look at the tools at our disposal.
  328 
  329 The Django test client
  330 ----------------------
  331 
  332 Django provides a test :class:`~django.test.Client` to simulate a user
  333 interacting with the code at the view level.  We can use it in ``tests.py``
  334 or even in the :djadmin:`shell`.
  335 
  336 We will start again with the :djadmin:`shell`, where we need to do a couple of
  337 things that won't be necessary in ``tests.py``. The first is to set up the test
  338 environment in the :djadmin:`shell`::
  339 
  340     >>> from django.test.utils import setup_test_environment
  341     >>> setup_test_environment()
  342 
  343 :meth:`~django.test.utils.setup_test_environment` installs a template renderer
  344 which will allow us to examine some additional attributes on responses such as
  345 ``response.context`` that otherwise wouldn't be available. Note that this
  346 method *does not* setup a test database, so the following will be run against
  347 the existing database and the output may differ slightly depending on what
  348 questions you already created. You might get unexpected results if your
  349 ``TIME_ZONE`` in ``settings.py`` isn't correct. If you don't remember setting
  350 it earlier, check it before continuing.
  351 
  352 Next we need to import the test client class (later in ``tests.py`` we will use
  353 the :class:`django.test.TestCase` class, which comes with its own client, so
  354 this won't be required)::
  355 
  356     >>> from django.test import Client
  357     >>> # create an instance of the client for our use
  358     >>> client = Client()
  359 
  360 With that ready, we can ask the client to do some work for us::
  361 
  362     >>> # get a response from '/'
  363     >>> response = client.get('/')
  364     Not Found: /
  365     >>> # we should expect a 404 from that address; if you instead see an
  366     >>> # "Invalid HTTP_HOST header" error and a 400 response, you probably
  367     >>> # omitted the setup_test_environment() call described earlier.
  368     >>> response.status_code
  369     404
  370     >>> # on the other hand we should expect to find something at '/polls/'
  371     >>> # we'll use 'reverse()' rather than a hardcoded URL
  372     >>> from django.urls import reverse
  373     >>> response = client.get(reverse('polls:index'))
  374     >>> response.status_code
  375     200
  376     >>> response.content
  377     b'\n    <ul>\n    \n        <li><a href="/polls/1/">What&#39;s up?</a></li>\n    \n    </ul>\n\n'
  378     >>> response.context['latest_question_list']
  379     <QuerySet [<Question: What's up?>]>
  380 
  381 Improving our view
  382 ------------------
  383 
  384 The list of polls shows polls that aren't published yet (i.e. those that have a
  385 ``pub_date`` in the future). Let's fix that.
  386 
  387 In :doc:`Tutorial 4 </intro/tutorial04>` we introduced a class-based view,
  388 based on :class:`~django.views.generic.list.ListView`:
  389 
  390 .. snippet::
  391     :filename: polls/views.py
  392 
  393     class IndexView(generic.ListView):
  394         template_name = 'polls/index.html'
  395         context_object_name = 'latest_question_list'
  396 
  397         def get_queryset(self):
  398             """Return the last five published questions."""
  399             return Question.objects.order_by('-pub_date')[:5]
  400 
  401 We need to amend the ``get_queryset()`` method and change it so that it also
  402 checks the date by comparing it with ``timezone.now()``. First we need to add
  403 an import:
  404 
  405 .. snippet::
  406     :filename: polls/views.py
  407 
  408     from django.utils import timezone
  409 
  410 and then we must amend the ``get_queryset`` method like so:
  411 
  412 .. snippet::
  413     :filename: polls/views.py
  414 
  415     def get_queryset(self):
  416         """
  417         Return the last five published questions (not including those set to be
  418         published in the future).
  419         """
  420         return Question.objects.filter(
  421             pub_date__lte=timezone.now()
  422         ).order_by('-pub_date')[:5]
  423 
  424 ``Question.objects.filter(pub_date__lte=timezone.now())`` returns a queryset
  425 containing ``Question``\s whose ``pub_date`` is less than or equal to - that
  426 is, earlier than or equal to - ``timezone.now``.
  427 
  428 Testing our new view
  429 --------------------
  430 
  431 Now you can satisfy yourself that this behaves as expected by firing up the
  432 runserver, loading the site in your browser, creating ``Questions`` with dates
  433 in the past and future, and checking that only those that have been published
  434 are listed.  You don't want to have to do that *every single time you make any
  435 change that might affect this* - so let's also create a test, based on our
  436 :djadmin:`shell` session above.
  437 
  438 Add the following to ``polls/tests.py``:
  439 
  440 .. snippet::
  441     :filename: polls/tests.py
  442 
  443     from django.urls import reverse
  444 
  445 and we'll create a shortcut function to create questions as well as a new test
  446 class:
  447 
  448 .. snippet::
  449     :filename: polls/tests.py
  450 
  451     def create_question(question_text, days):
  452         """
  453         Create a question with the given `question_text` and published the
  454         given number of `days` offset to now (negative for questions published
  455         in the past, positive for questions that have yet to be published).
  456         """
  457         time = timezone.now() + datetime.timedelta(days=days)
  458         return Question.objects.create(question_text=question_text, pub_date=time)
  459 
  460 
  461     class QuestionIndexViewTests(TestCase):
  462         def test_no_questions(self):
  463             """
  464             If no questions exist, an appropriate message is displayed.
  465             """
  466             response = self.client.get(reverse('polls:index'))
  467             self.assertEqual(response.status_code, 200)
  468             self.assertContains(response, "No polls are available.")
  469             self.assertQuerysetEqual(response.context['latest_question_list'], [])
  470 
  471         def test_past_question(self):
  472             """
  473             Questions with a pub_date in the past are displayed on the
  474             index page.
  475             """
  476             create_question(question_text="Past question.", days=-30)
  477             response = self.client.get(reverse('polls:index'))
  478             self.assertQuerysetEqual(
  479                 response.context['latest_question_list'],
  480                 ['<Question: Past question.>']
  481             )
  482 
  483         def test_future_question(self):
  484             """
  485             Questions with a pub_date in the future aren't displayed on
  486             the index page.
  487             """
  488             create_question(question_text="Future question.", days=30)
  489             response = self.client.get(reverse('polls:index'))
  490             self.assertContains(response, "No polls are available.")
  491             self.assertQuerysetEqual(response.context['latest_question_list'], [])
  492 
  493         def test_future_question_and_past_question(self):
  494             """
  495             Even if both past and future questions exist, only past questions
  496             are displayed.
  497             """
  498             create_question(question_text="Past question.", days=-30)
  499             create_question(question_text="Future question.", days=30)
  500             response = self.client.get(reverse('polls:index'))
  501             self.assertQuerysetEqual(
  502                 response.context['latest_question_list'],
  503                 ['<Question: Past question.>']
  504             )
  505 
  506         def test_two_past_questions(self):
  507             """
  508             The questions index page may display multiple questions.
  509             """
  510             create_question(question_text="Past question 1.", days=-30)
  511             create_question(question_text="Past question 2.", days=-5)
  512             response = self.client.get(reverse('polls:index'))
  513             self.assertQuerysetEqual(
  514                 response.context['latest_question_list'],
  515                 ['<Question: Past question 2.>', '<Question: Past question 1.>']
  516             )
  517 
  518 
  519 Let's look at some of these more closely.
  520 
  521 First is a question shortcut function, ``create_question``, to take some
  522 repetition out of the process of creating questions.
  523 
  524 ``test_no_questions`` doesn't create any questions, but checks the message:
  525 "No polls are available." and verifies the ``latest_question_list`` is empty.
  526 Note that the :class:`django.test.TestCase` class provides some additional
  527 assertion methods. In these examples, we use
  528 :meth:`~django.test.SimpleTestCase.assertContains()` and
  529 :meth:`~django.test.TransactionTestCase.assertQuerysetEqual()`.
  530 
  531 In ``test_past_question``, we create a question and verify that it appears in
  532 the list.
  533 
  534 In ``test_future_question``, we create a question with a ``pub_date`` in the
  535 future. The database is reset for each test method, so the first question is no
  536 longer there, and so again the index shouldn't have any questions in it.
  537 
  538 And so on. In effect, we are using the tests to tell a story of admin input
  539 and user experience on the site, and checking that at every state and for every
  540 new change in the state of the system, the expected results are published.
  541 
  542 Testing the ``DetailView``
  543 --------------------------
  544 
  545 What we have works well; however, even though future questions don't appear in
  546 the *index*, users can still reach them if they know or guess the right URL. So
  547 we need to add a similar  constraint to ``DetailView``:
  548 
  549 .. snippet::
  550     :filename: polls/views.py
  551 
  552     class DetailView(generic.DetailView):
  553         ...
  554         def get_queryset(self):
  555             """
  556             Excludes any questions that aren't published yet.
  557             """
  558             return Question.objects.filter(pub_date__lte=timezone.now())
  559 
  560 And of course, we will add some tests, to check that a ``Question`` whose
  561 ``pub_date`` is in the past can be displayed, and that one with a ``pub_date``
  562 in the future is not:
  563 
  564 .. snippet::
  565     :filename: polls/tests.py
  566 
  567     class QuestionDetailViewTests(TestCase):
  568         def test_future_question(self):
  569             """
  570             The detail view of a question with a pub_date in the future
  571             returns a 404 not found.
  572             """
  573             future_question = create_question(question_text='Future question.', days=5)
  574             url = reverse('polls:detail', args=(future_question.id,))
  575             response = self.client.get(url)
  576             self.assertEqual(response.status_code, 404)
  577 
  578         def test_past_question(self):
  579             """
  580             The detail view of a question with a pub_date in the past
  581             displays the question's text.
  582             """
  583             past_question = create_question(question_text='Past Question.', days=-5)
  584             url = reverse('polls:detail', args=(past_question.id,))
  585             response = self.client.get(url)
  586             self.assertContains(response, past_question.question_text)
  587 
  588 Ideas for more tests
  589 --------------------
  590 
  591 We ought to add a similar ``get_queryset`` method to ``ResultsView`` and
  592 create a new test class for that view. It'll be very similar to what we have
  593 just created; in fact there will be a lot of repetition.
  594 
  595 We could also improve our application in other ways, adding tests along the
  596 way. For example, it's silly that ``Questions`` can be published on the site
  597 that have no ``Choices``. So, our views could check for this, and exclude such
  598 ``Questions``. Our tests would create a ``Question`` without ``Choices`` and
  599 then test that it's not published, as well as create a similar ``Question``
  600 *with* ``Choices``, and test that it *is* published.
  601 
  602 Perhaps logged-in admin users should be allowed to see unpublished
  603 ``Questions``, but not ordinary visitors. Again: whatever needs to be added to
  604 the software to accomplish this should be accompanied by a test, whether you
  605 write the test first and then make the code pass the test, or work out the
  606 logic in your code first and then write a test to prove it.
  607 
  608 At a certain point you are bound to look at your tests and wonder whether your
  609 code is suffering from test bloat, which brings us to:
  610 
  611 When testing, more is better
  612 ============================
  613 
  614 It might seem that our tests are growing out of control. At this rate there will
  615 soon be more code in our tests than in our application, and the repetition
  616 is unaesthetic, compared to the elegant conciseness of the rest of our code.
  617 
  618 **It doesn't matter**. Let them grow. For the most part, you can write a test
  619 once and then forget about it. It will continue performing its useful function
  620 as you continue to develop your program.
  621 
  622 Sometimes tests will need to be updated. Suppose that we amend our views so that
  623 only ``Questions`` with ``Choices`` are published. In that case, many of our
  624 existing tests will fail - *telling us exactly which tests need to be amended to
  625 bring them up to date*, so to that extent tests help look after themselves.
  626 
  627 At worst, as you continue developing, you might find that you have some tests
  628 that are now redundant. Even that's not a problem; in testing redundancy is
  629 a *good* thing.
  630 
  631 As long as your tests are sensibly arranged, they won't become unmanageable.
  632 Good rules-of-thumb include having:
  633 
  634 * a separate ``TestClass`` for each model or view
  635 * a separate test method for each set of conditions you want to test
  636 * test method names that describe their function
  637 
  638 Further testing
  639 ===============
  640 
  641 This tutorial only introduces some of the basics of testing. There's a great
  642 deal more you can do, and a number of very useful tools at your disposal to
  643 achieve some very clever things.
  644 
  645 For example, while our tests here have covered some of the internal logic of a
  646 model and the way our views publish information, you can use an "in-browser"
  647 framework such as Selenium_ to test the way your HTML actually renders in a
  648 browser. These tools allow you to check not just the behavior of your Django
  649 code, but also, for example, of your JavaScript. It's quite something to see
  650 the tests launch a browser, and start interacting with your site, as if a human
  651 being were driving it! Django includes :class:`~django.test.LiveServerTestCase`
  652 to facilitate integration with tools like Selenium.
  653 
  654 If you have a complex application, you may want to run tests automatically
  655 with every commit for the purposes of `continuous integration`_, so that
  656 quality control is itself - at least partially - automated.
  657 
  658 A good way to spot untested parts of your application is to check code
  659 coverage. This also helps identify fragile or even dead code. If you can't test
  660 a piece of code, it usually means that code should be refactored or removed.
  661 Coverage will help to identify dead code. See
  662 :ref:`topics-testing-code-coverage` for details.
  663 
  664 :doc:`Testing in Django </topics/testing/index>` has comprehensive
  665 information about testing.
  666 
  667 .. _Selenium: http://seleniumhq.org/
  668 .. _continuous integration: https://en.wikipedia.org/wiki/Continuous_integration
  669 
  670 What's next?
  671 ============
  672 
  673 For full details on testing, see :doc:`Testing in Django
  674 </topics/testing/index>`.
  675 
  676 When you're comfortable with testing Django views, read
  677 :doc:`part 6 of this tutorial</intro/tutorial06>` to learn about
  678 static files management.