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