2012-12-15 14:03:17 +01:00
|
|
|
|
=====================================
|
|
|
|
|
Writing your first Django app, part 5
|
|
|
|
|
=====================================
|
|
|
|
|
|
|
|
|
|
This tutorial begins where :doc:`Tutorial 4 </intro/tutorial04>` left off.
|
|
|
|
|
We've built a Web-poll application, and we'll now create some automated tests
|
|
|
|
|
for it.
|
|
|
|
|
|
|
|
|
|
Introducing automated testing
|
|
|
|
|
=============================
|
|
|
|
|
|
|
|
|
|
What are automated tests?
|
|
|
|
|
-------------------------
|
|
|
|
|
|
|
|
|
|
Tests are simple routines that check the operation of your code.
|
|
|
|
|
|
|
|
|
|
Testing operates at different levels. Some tests might apply to a tiny detail
|
2013-05-08 01:07:29 +02:00
|
|
|
|
(*does a particular model method return values as expected?*) while others
|
|
|
|
|
examine the overall operation of the software (*does a sequence of user inputs
|
|
|
|
|
on the site produce the desired result?*). That's no different from the kind of
|
2012-12-15 14:03:17 +01:00
|
|
|
|
testing you did earlier in :doc:`Tutorial 1 </intro/tutorial01>`, using the
|
|
|
|
|
shell to examine the behavior of a method, or running the application and
|
|
|
|
|
entering data to check how it behaves.
|
|
|
|
|
|
|
|
|
|
What's different in *automated* tests is that the testing work is done for
|
|
|
|
|
you by the system. You create a set of tests once, and then as you make changes
|
|
|
|
|
to your app, you can check that your code still works as you originally
|
|
|
|
|
intended, without having to perform time consuming manual testing.
|
|
|
|
|
|
|
|
|
|
Why you need to create tests
|
|
|
|
|
----------------------------
|
|
|
|
|
|
|
|
|
|
So why create tests, and why now?
|
|
|
|
|
|
|
|
|
|
You may feel that you have quite enough on your plate just learning
|
|
|
|
|
Python/Django, and having yet another thing to learn and do may seem
|
|
|
|
|
overwhelming and perhaps unnecessary. After all, our polls application is
|
|
|
|
|
working quite happily now; going through the trouble of creating automated
|
|
|
|
|
tests is not going to make it work any better. If creating the polls
|
|
|
|
|
application is the last bit of Django programming you will ever do, then true,
|
|
|
|
|
you don't need to know how to create automated tests. But, if that's not the
|
|
|
|
|
case, now is an excellent time to learn.
|
|
|
|
|
|
|
|
|
|
Tests will save you time
|
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
|
|
Up to a certain point, 'checking that it seems to work' will be a satisfactory
|
|
|
|
|
test. In a more sophisticated application, you might have dozens of complex
|
|
|
|
|
interactions between components.
|
|
|
|
|
|
|
|
|
|
A change in any of those components could have unexpected consequences on the
|
|
|
|
|
application's behavior. Checking that it still 'seems to work' could mean
|
|
|
|
|
running through your code's functionality with twenty different variations of
|
|
|
|
|
your test data just to make sure you haven't broken something - not a good use
|
|
|
|
|
of your time.
|
|
|
|
|
|
|
|
|
|
That's especially true when automated tests could do this for you in seconds.
|
|
|
|
|
If something's gone wrong, tests will also assist in identifying the code
|
|
|
|
|
that's causing the unexpected behavior.
|
|
|
|
|
|
|
|
|
|
Sometimes it may seem a chore to tear yourself away from your productive,
|
|
|
|
|
creative programming work to face the unglamorous and unexciting business
|
|
|
|
|
of writing tests, particularly when you know your code is working properly.
|
|
|
|
|
|
|
|
|
|
However, the task of writing tests is a lot more fulfilling than spending hours
|
|
|
|
|
testing your application manually or trying to identify the cause of a
|
|
|
|
|
newly-introduced problem.
|
|
|
|
|
|
|
|
|
|
Tests don't just identify problems, they prevent them
|
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
|
|
It's a mistake to think of tests merely as a negative aspect of development.
|
|
|
|
|
|
|
|
|
|
Without tests, the purpose or intended behavior of an application might be
|
|
|
|
|
rather opaque. Even when it's your own code, you will sometimes find yourself
|
|
|
|
|
poking around in it trying to find out what exactly it's doing.
|
|
|
|
|
|
|
|
|
|
Tests change that; they light up your code from the inside, and when something
|
|
|
|
|
goes wrong, they focus light on the part that has gone wrong - *even if you
|
|
|
|
|
hadn't even realized it had gone wrong*.
|
|
|
|
|
|
|
|
|
|
Tests make your code more attractive
|
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
|
|
You might have created a brilliant piece of software, but you will find that
|
|
|
|
|
many other developers will simply refuse to look at it because it lacks tests;
|
|
|
|
|
without tests, they won't trust it. Jacob Kaplan-Moss, one of Django's
|
|
|
|
|
original developers, says "Code without tests is broken by design."
|
|
|
|
|
|
|
|
|
|
That other developers want to see tests in your software before they take it
|
|
|
|
|
seriously is yet another reason for you to start writing tests.
|
|
|
|
|
|
|
|
|
|
Tests help teams work together
|
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
|
|
The previous points are written from the point of view of a single developer
|
|
|
|
|
maintaining an application. Complex applications will be maintained by teams.
|
|
|
|
|
Tests guarantee that colleagues don't inadvertently break your code (and that
|
|
|
|
|
you don't break theirs without knowing). If you want to make a living as a
|
|
|
|
|
Django programmer, you must be good at writing tests!
|
|
|
|
|
|
|
|
|
|
Basic testing strategies
|
|
|
|
|
========================
|
|
|
|
|
|
|
|
|
|
There are many ways to approach writing tests.
|
|
|
|
|
|
|
|
|
|
Some programmers follow a discipline called "`test-driven development`_"; they
|
|
|
|
|
actually write their tests before they write their code. This might seem
|
|
|
|
|
counter-intuitive, but in fact it's similar to what most people will often do
|
|
|
|
|
anyway: they describe a problem, then create some code to solve it. Test-driven
|
|
|
|
|
development simply formalizes the problem in a Python test case.
|
|
|
|
|
|
|
|
|
|
More often, a newcomer to testing will create some code and later decide that
|
|
|
|
|
it should have some tests. Perhaps it would have been better to write some
|
|
|
|
|
tests earlier, but it's never too late to get started.
|
|
|
|
|
|
|
|
|
|
Sometimes it's difficult to figure out where to get started with writing tests.
|
|
|
|
|
If you have written several thousand lines of Python, choosing something to
|
|
|
|
|
test might not be easy. In such a case, it's fruitful to write your first test
|
|
|
|
|
the next time you make a change, either when you add a new feature or fix a bug.
|
|
|
|
|
|
|
|
|
|
So let's do that right away.
|
|
|
|
|
|
2013-03-10 15:57:51 +01:00
|
|
|
|
.. _test-driven development: http://en.wikipedia.org/wiki/Test-driven_development
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
Writing our first test
|
|
|
|
|
======================
|
|
|
|
|
|
|
|
|
|
We identify a bug
|
|
|
|
|
-----------------
|
|
|
|
|
|
|
|
|
|
Fortunately, there's a little bug in the ``polls`` application for us to fix
|
2013-09-06 20:57:00 +02:00
|
|
|
|
right away: the ``Question.was_published_recently()`` method returns ``True`` if
|
|
|
|
|
the ``Question`` was published within the last day (which is correct) but also if
|
|
|
|
|
the ``Question``’s ``pub_date`` field is in the future (which certainly isn't).
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
You can see this in the Admin; create a question whose date lies in the future;
|
|
|
|
|
you'll see that the ``Question`` change list claims it was published recently.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
You can also see this using the shell::
|
|
|
|
|
|
|
|
|
|
>>> import datetime
|
|
|
|
|
>>> from django.utils import timezone
|
2013-09-06 20:57:00 +02:00
|
|
|
|
>>> from polls.models import Question
|
|
|
|
|
>>> # create a Question instance with pub_date 30 days in the future
|
|
|
|
|
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
|
2012-12-15 14:03:17 +01:00
|
|
|
|
>>> # was it published recently?
|
2013-09-06 20:57:00 +02:00
|
|
|
|
>>> future_question.was_published_recently()
|
2012-12-15 14:03:17 +01:00
|
|
|
|
True
|
|
|
|
|
|
|
|
|
|
Since things in the future are not 'recent', this is clearly wrong.
|
|
|
|
|
|
|
|
|
|
Create a test to expose the bug
|
|
|
|
|
-------------------------------
|
|
|
|
|
|
|
|
|
|
What we've just done in the shell to test for the problem is exactly what we
|
|
|
|
|
can do in an automated test, so let's turn that into an automated test.
|
|
|
|
|
|
2013-05-11 05:08:45 +02:00
|
|
|
|
A conventional place for an application's tests is in the application's
|
|
|
|
|
``tests.py`` file; the testing system will automatically find tests in any file
|
|
|
|
|
whose name begins with ``test``.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
Simplified default project template.
Squashed commit of:
commit 508ec9144b35c50794708225b496bde1eb5e60aa
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Jan 29 22:50:55 2013 +0100
Tweaked default settings file.
* Explained why BASE_DIR exists.
* Added a link to the database configuration options, and put it in its
own section.
* Moved sensitive settings that must be changed for production at the
top.
commit 6515fd2f1aa73a86dc8dbd2ccf512ddb6b140d57
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Jan 29 14:35:21 2013 +0100
Documented the simplified app & project templates in the changelog.
commit 2c5b576c2ea91d84273a019b3d0b3b8b4da72f23
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Jan 29 13:59:27 2013 +0100
Minor fixes in tutorials 5 and 6.
commit 55a51531be8104f21b3cca3f6bf70b0a7139a041
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Jan 29 13:51:11 2013 +0100
Updated tutorial 2 for the new project template.
commit 29ddae87bdaecff12dd31b16b000c01efbde9e20
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Jan 29 11:58:54 2013 +0100
Updated tutorial 1 for the new project template.
commit 0ecb9f6e2514cfd26a678a280d471433375101a3
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Jan 29 11:29:13 2013 +0100
Adjusted the default URLconf detection to account for the admin.
It's now enabled by default.
commit 5fb4da0d3d09dac28dd94e3fde92b9d4335c0565
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Jan 29 10:36:55 2013 +0100
Added security warnings for the most sensitive settings.
commit 718d84bd8ac4a42fb4b28ec93965de32680f091e
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 23:24:06 2013 +0100
Used an absolute path for the SQLite database.
This ensures the settings file works regardless of which directory
django-admin.py / manage.py is invoked from.
BASE_DIR got a +1 from a BDFL and another core dev. It doesn't involve
the concept of a "Django project"; it's just a convenient way to express
relative paths within the source code repository for non-Python files.
Thanks Jacob Kaplan-Moss for the suggestion.
commit 1b559b4bcda622e10909b68fe5cab90db6727dd9
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 23:22:40 2013 +0100
Removed STATIC_ROOT from the default settings template.
It isn't necessary in development, and it confuses beginners to no end.
Thanks Carl Meyer for the suggestion.
commit a55f141a500bb7c9a1bc259bbe1954c13b199671
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 23:21:43 2013 +0100
Removed MEDIA_ROOT/URL from default settings template.
Many sites will never deal with user-uploaded files, and MEDIA_ROOT is
complicated to explain.
Thanks Carl Meyer for the suggestion.
commit 44bf2f2441420fd9429ee9fe1f7207f92dd87e70
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 22:22:09 2013 +0100
Removed logging config.
This configuration is applied regardless of the value of LOGGING;
duplicating it in LOGGING is confusing.
commit eac747e848eaed65fd5f6f254f0a7559d856f88f
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 22:05:31 2013 +0100
Enabled the locale middleware by default.
USE_I18N is True by default, and doesn't work well without
LocaleMiddleware.
commit d806c62b2d00826dc2688c84b092627b8d571cab
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 22:03:16 2013 +0100
Enabled clickjacking protection by default.
commit 99152c30e6a15003f0b6737dc78e87adf462aacb
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 22:01:48 2013 +0100
Reorganized settings in logical sections, and trimmed comments.
commit d37ffdfcb24b7e0ec7cc113d07190f65fb12fb8a
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 16:54:11 2013 +0100
Avoided misleading TEMPLATE_DEBUG = DEBUG.
According to the docs TEMPLATE_DEBUG works only when DEBUG = True.
commit 15d9478d3a9850e85841e7cf09cf83050371c6bf
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 16:46:25 2013 +0100
Removed STATICFILES_FINDERS/TEMPLATE_LOADERS from default settings file.
Only developers with special needs ever need to change these settings.
commit 574da0eb5bfb4570883756914b4dbd7e20e1f61e
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 16:45:01 2013 +0100
Removed STATICFILES/TEMPLATES_DIRS from default settings file.
The current best practice is to put static files and templates in
applications, for easier testing and deployment.
commit 8cb18dbe56629aa1be74718a07e7cc66b4f9c9f0
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 16:24:16 2013 +0100
Removed settings related to email reporting from default settings file.
While handy for small scale projects, it isn't exactly a best practice.
commit 8ecbfcb3638058f0c49922540f874a7d802d864f
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Jan 29 18:54:43 2013 +0100
Documented how to enable the sites framework.
commit 23fc91a6fa67d91ddd9d71b1c3e0dc26bdad9841
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 16:28:59 2013 +0100
Disabled the sites framework by default.
RequestSite does the job for single-domain websites.
commit c4d82eb8afc0eb8568bf9c4d12644272415e3960
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Jan 29 00:08:33 2013 +0100
Added a default admin.py to the application template.
Thanks Ryan D Hiebert for the suggestion.
commit 4071dc771e5c44b1c5ebb9beecefb164ae465e22
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 10:59:49 2013 +0100
Enabled the admin by default.
Everyone uses the admin.
commit c807a31f8d89e7e7fd97380e3023f7983a8b6fcb
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 10:57:05 2013 +0100
Removed admindocs from default project template.
commit 09e4ce0e652a97da1a9e285046a91c8ad7a9189c
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 16:32:52 2013 +0100
Added links to the settings documentation.
commit 5b8f5eaef364eb790fcde6f9e86f7d266074cca8
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 11:06:54 2013 +0100
Used a significant example for URLconf includes.
commit 908e91d6fcee2a3cb51ca26ecdf12a6a24e69ef8
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 16:22:31 2013 +0100
Moved code comments about WSGI to docs, and rewrote said docs.
commit 50417e51996146f891d08ca8b74dcc736a581932
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Mon Jan 28 15:51:50 2013 +0100
Normalized the default application template.
Removed the default test that 1 + 1 = 2, because it's been committed
way too many times, in too many projects.
Added an import of `render` for views, because the first view will
often be:
def home(request):
return render(request, "mysite/home.html")
2013-01-28 15:51:50 +01:00
|
|
|
|
Put the following in the ``tests.py`` file in the ``polls`` application::
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
import datetime
|
|
|
|
|
|
|
|
|
|
from django.utils import timezone
|
|
|
|
|
from django.test import TestCase
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
from polls.models import Question
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
class QuestionMethodTests(TestCase):
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
def test_was_published_recently_with_future_question(self):
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
was_published_recently() should return False for questions whose
|
2012-12-15 14:03:17 +01:00
|
|
|
|
pub_date is in the future
|
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
time = timezone.now() + datetime.timedelta(days=30)
|
|
|
|
|
future_question = Question(pub_date=time)
|
|
|
|
|
self.assertEqual(future_question.was_published_recently(), False)
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
What we have done here is created a :class:`django.test.TestCase` subclass
|
2013-09-06 20:57:00 +02:00
|
|
|
|
with a method that creates a ``Question`` instance with a ``pub_date`` in the
|
2012-12-15 14:03:17 +01:00
|
|
|
|
future. We then check the output of ``was_published_recently()`` - which
|
|
|
|
|
*ought* to be False.
|
|
|
|
|
|
|
|
|
|
Running tests
|
|
|
|
|
-------------
|
|
|
|
|
|
|
|
|
|
In the terminal, we can run our test::
|
|
|
|
|
|
2013-09-18 16:35:41 +02:00
|
|
|
|
$ python manage.py test polls
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
and you'll see something like::
|
|
|
|
|
|
|
|
|
|
Creating test database for alias 'default'...
|
|
|
|
|
F
|
|
|
|
|
======================================================================
|
2013-09-06 20:57:00 +02:00
|
|
|
|
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests)
|
2012-12-15 14:03:17 +01:00
|
|
|
|
----------------------------------------------------------------------
|
|
|
|
|
Traceback (most recent call last):
|
2013-09-06 20:57:00 +02:00
|
|
|
|
File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
|
|
|
|
|
self.assertEqual(future_question.was_published_recently(), False)
|
2012-12-15 14:03:17 +01:00
|
|
|
|
AssertionError: True != False
|
|
|
|
|
|
|
|
|
|
----------------------------------------------------------------------
|
|
|
|
|
Ran 1 test in 0.001s
|
|
|
|
|
|
|
|
|
|
FAILED (failures=1)
|
|
|
|
|
Destroying test database for alias 'default'...
|
|
|
|
|
|
|
|
|
|
What happened is this:
|
|
|
|
|
|
|
|
|
|
* ``python manage.py test polls`` looked for tests in the ``polls`` application
|
|
|
|
|
|
|
|
|
|
* it found a subclass of the :class:`django.test.TestCase` class
|
|
|
|
|
|
|
|
|
|
* it created a special database for the purpose of testing
|
|
|
|
|
|
|
|
|
|
* it looked for test methods - ones whose names begin with ``test``
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
* in ``test_was_published_recently_with_future_question`` it created a ``Question``
|
2012-12-15 14:03:17 +01:00
|
|
|
|
instance whose ``pub_date`` field is 30 days in the future
|
|
|
|
|
|
|
|
|
|
* ... and using the ``assertEqual()`` method, it discovered that its
|
|
|
|
|
``was_published_recently()`` returns ``True``, though we wanted it to return
|
|
|
|
|
``False``
|
|
|
|
|
|
|
|
|
|
The test informs us which test failed and even the line on which the failure
|
|
|
|
|
occurred.
|
|
|
|
|
|
|
|
|
|
Fixing the bug
|
|
|
|
|
--------------
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
We already know what the problem is: ``Question.was_published_recently()`` should
|
2012-12-15 14:03:17 +01:00
|
|
|
|
return ``False`` if its ``pub_date`` is in the future. Amend the method in
|
|
|
|
|
``models.py``, so that it will only return ``True`` if the date is also in the
|
|
|
|
|
past::
|
|
|
|
|
|
|
|
|
|
def was_published_recently(self):
|
|
|
|
|
now = timezone.now()
|
2013-09-06 20:57:00 +02:00
|
|
|
|
return now - datetime.timedelta(days=1) <= self.pub_date < now
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
and run the test again::
|
|
|
|
|
|
|
|
|
|
Creating test database for alias 'default'...
|
|
|
|
|
.
|
|
|
|
|
----------------------------------------------------------------------
|
|
|
|
|
Ran 1 test in 0.001s
|
|
|
|
|
|
|
|
|
|
OK
|
|
|
|
|
Destroying test database for alias 'default'...
|
|
|
|
|
|
|
|
|
|
After identifying a bug, we wrote a test that exposes it and corrected the bug
|
|
|
|
|
in the code so our test passes.
|
|
|
|
|
|
|
|
|
|
Many other things might go wrong with our application in the future, but we can
|
|
|
|
|
be sure that we won't inadvertently reintroduce this bug, because simply
|
|
|
|
|
running the test will warn us immediately. We can consider this little portion
|
|
|
|
|
of the application pinned down safely forever.
|
|
|
|
|
|
|
|
|
|
More comprehensive tests
|
|
|
|
|
------------------------
|
|
|
|
|
|
|
|
|
|
While we're here, we can further pin down the ``was_published_recently()``
|
|
|
|
|
method; in fact, it would be positively embarrassing if in fixing one bug we had
|
|
|
|
|
introduced another.
|
|
|
|
|
|
|
|
|
|
Add two more test methods to the same class, to test the behavior of the method
|
|
|
|
|
more comprehensively::
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
def test_was_published_recently_with_old_question(self):
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
was_published_recently() should return False for questions whose
|
|
|
|
|
pub_date is older than 1 day
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
time = timezone.now() - datetime.timedelta(days=30)
|
|
|
|
|
old_question = Question(pub_date=time)
|
|
|
|
|
self.assertEqual(old_question.was_published_recently(), False)
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
def test_was_published_recently_with_recent_question(self):
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
was_published_recently() should return True for questions whose
|
|
|
|
|
pub_date is within the last day
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
time = timezone.now() - datetime.timedelta(hours=1)
|
|
|
|
|
recent_question = Question(pub_date=time)
|
|
|
|
|
self.assertEqual(recent_question.was_published_recently(), True)
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
And now we have three tests that confirm that ``Question.was_published_recently()``
|
|
|
|
|
returns sensible values for past, recent, and future questions.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
Again, ``polls`` is a simple application, but however complex it grows in the
|
|
|
|
|
future and whatever other code it interacts with, we now have some guarantee
|
|
|
|
|
that the method we have written tests for will behave in expected ways.
|
|
|
|
|
|
|
|
|
|
Test a view
|
|
|
|
|
===========
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
The polls application is fairly undiscriminating: it will publish any question,
|
2012-12-15 14:03:17 +01:00
|
|
|
|
including ones whose ``pub_date`` field lies in the future. We should improve
|
2013-09-06 20:57:00 +02:00
|
|
|
|
this. Setting a ``pub_date`` in the future should mean that the Question is
|
2012-12-15 14:03:17 +01:00
|
|
|
|
published at that moment, but invisible until then.
|
|
|
|
|
|
|
|
|
|
A test for a view
|
|
|
|
|
-----------------
|
|
|
|
|
|
|
|
|
|
When we fixed the bug above, we wrote the test first and then the code to fix
|
|
|
|
|
it. In fact that was a simple example of test-driven development, but it
|
|
|
|
|
doesn't really matter in which order we do the work.
|
|
|
|
|
|
|
|
|
|
In our first test, we focused closely on the internal behavior of the code. For
|
|
|
|
|
this test, we want to check its behavior as it would be experienced by a user
|
|
|
|
|
through a web browser.
|
|
|
|
|
|
|
|
|
|
Before we try to fix anything, let's have a look at the tools at our disposal.
|
|
|
|
|
|
|
|
|
|
The Django test client
|
|
|
|
|
----------------------
|
|
|
|
|
|
2013-09-09 10:59:47 +02:00
|
|
|
|
Django provides a test :class:`~django.test.Client` to simulate a user
|
2012-12-15 14:03:17 +01:00
|
|
|
|
interacting with the code at the view level. We can use it in ``tests.py``
|
|
|
|
|
or even in the shell.
|
|
|
|
|
|
|
|
|
|
We will start again with the shell, where we need to do a couple of things that
|
|
|
|
|
won't be necessary in ``tests.py``. The first is to set up the test environment
|
|
|
|
|
in the shell::
|
|
|
|
|
|
|
|
|
|
>>> from django.test.utils import setup_test_environment
|
|
|
|
|
>>> setup_test_environment()
|
|
|
|
|
|
2013-05-06 19:55:02 +02:00
|
|
|
|
:meth:`~django.test.utils.setup_test_environment` installs a template renderer
|
|
|
|
|
which will allow us to examine some additional attributes on responses such as
|
|
|
|
|
``response.context`` that otherwise wouldn't be available. Note that this
|
|
|
|
|
method *does not* setup a test database, so the following will be run against
|
|
|
|
|
the existing database and the output may differ slightly depending on what
|
2013-09-06 20:57:00 +02:00
|
|
|
|
questions you already created.
|
2013-05-06 19:55:02 +02:00
|
|
|
|
|
2012-12-15 14:03:17 +01:00
|
|
|
|
Next we need to import the test client class (later in ``tests.py`` we will use
|
|
|
|
|
the :class:`django.test.TestCase` class, which comes with its own client, so
|
|
|
|
|
this won't be required)::
|
|
|
|
|
|
2013-09-09 10:59:47 +02:00
|
|
|
|
>>> from django.test import Client
|
2012-12-15 14:03:17 +01:00
|
|
|
|
>>> # create an instance of the client for our use
|
|
|
|
|
>>> client = Client()
|
|
|
|
|
|
|
|
|
|
With that ready, we can ask the client to do some work for us::
|
|
|
|
|
|
|
|
|
|
>>> # get a response from '/'
|
|
|
|
|
>>> response = client.get('/')
|
|
|
|
|
>>> # we should expect a 404 from that address
|
|
|
|
|
>>> response.status_code
|
|
|
|
|
404
|
|
|
|
|
>>> # on the other hand we should expect to find something at '/polls/'
|
|
|
|
|
>>> # we'll use 'reverse()' rather than a harcoded URL
|
|
|
|
|
>>> from django.core.urlresolvers import reverse
|
|
|
|
|
>>> response = client.get(reverse('polls:index'))
|
|
|
|
|
>>> response.status_code
|
|
|
|
|
200
|
|
|
|
|
>>> response.content
|
|
|
|
|
'\n\n\n <p>No polls are available.</p>\n\n'
|
|
|
|
|
>>> # note - you might get unexpected results if your ``TIME_ZONE``
|
|
|
|
|
>>> # in ``settings.py`` is not correct. If you need to change it,
|
|
|
|
|
>>> # you will also need to restart your shell session
|
2013-09-06 20:57:00 +02:00
|
|
|
|
>>> from polls.models import Question
|
2012-12-15 14:03:17 +01:00
|
|
|
|
>>> from django.utils import timezone
|
2013-09-06 20:57:00 +02:00
|
|
|
|
>>> # create a Question and save it
|
|
|
|
|
>>> q = Question(question_text="Who is your favorite Beatle?", pub_date=timezone.now())
|
|
|
|
|
>>> q.save()
|
2012-12-15 14:03:17 +01:00
|
|
|
|
>>> # check the response once again
|
|
|
|
|
>>> response = client.get('/polls/')
|
|
|
|
|
>>> response.content
|
|
|
|
|
'\n\n\n <ul>\n \n <li><a href="/polls/1/">Who is your favorite Beatle?</a></li>\n \n </ul>\n\n'
|
2013-09-06 20:57:00 +02:00
|
|
|
|
>>> response.context['latest_question_list']
|
|
|
|
|
[<Question: Who is your favorite Beatle?>]
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
Improving our view
|
|
|
|
|
------------------
|
|
|
|
|
|
|
|
|
|
The list of polls shows polls that aren't published yet (i.e. those that have a
|
|
|
|
|
``pub_date`` in the future). Let's fix that.
|
|
|
|
|
|
2013-05-12 01:08:57 +02:00
|
|
|
|
In :doc:`Tutorial 4 </intro/tutorial04>` we introduced a class-based view,
|
|
|
|
|
based on :class:`~django.views.generic.list.ListView`::
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-05-12 01:08:57 +02:00
|
|
|
|
class IndexView(generic.ListView):
|
|
|
|
|
template_name = 'polls/index.html'
|
2013-09-06 20:57:00 +02:00
|
|
|
|
context_object_name = 'latest_question_list'
|
2013-05-12 01:08:57 +02:00
|
|
|
|
|
|
|
|
|
def get_queryset(self):
|
2013-09-06 20:57:00 +02:00
|
|
|
|
"""Return the last five published questions."""
|
|
|
|
|
return Question.objects.order_by('-pub_date')[:5]
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
``response.context_data['latest_question_list']`` extracts the data this view
|
2012-12-15 14:03:17 +01:00
|
|
|
|
places into the context.
|
|
|
|
|
|
2013-05-12 01:08:57 +02:00
|
|
|
|
We need to amend the ``get_queryset`` method and change it so that it also
|
|
|
|
|
checks the date by comparing it with ``timezone.now()``. First we need to add
|
|
|
|
|
an import::
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
from django.utils import timezone
|
|
|
|
|
|
2013-05-12 01:08:57 +02:00
|
|
|
|
and then we must amend the ``get_queryset`` method like so::
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-05-12 01:08:57 +02:00
|
|
|
|
def get_queryset(self):
|
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
Return the last five published questions (not including those set to be
|
2013-05-12 01:08:57 +02:00
|
|
|
|
published in the future).
|
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
return Question.objects.filter(
|
2013-05-12 01:08:57 +02:00
|
|
|
|
pub_date__lte=timezone.now()
|
|
|
|
|
).order_by('-pub_date')[:5]
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
``Question.objects.filter(pub_date__lte=timezone.now())`` returns a queryset
|
|
|
|
|
containing ``Question``\s whose ``pub_date`` is less than or equal to - that
|
|
|
|
|
is, earlier than or equal to - ``timezone.now``.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
Testing our new view
|
|
|
|
|
--------------------
|
|
|
|
|
|
|
|
|
|
Now you can satisfy yourself that this behaves as expected by firing up the
|
2013-09-06 20:57:00 +02:00
|
|
|
|
runserver, loading the site in your browser, creating ``Questions`` with dates
|
|
|
|
|
in the past and future, and checking that only those that have been published
|
|
|
|
|
are listed. You don't want to have to do that *every single time you make any
|
2012-12-15 14:03:17 +01:00
|
|
|
|
change that might affect this* - so let's also create a test, based on our
|
|
|
|
|
shell session above.
|
|
|
|
|
|
|
|
|
|
Add the following to ``polls/tests.py``::
|
|
|
|
|
|
|
|
|
|
from django.core.urlresolvers import reverse
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
and we'll create a factory method to create questions as well as a new test
|
|
|
|
|
class::
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
def create_question(question_text, days):
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
Creates a question with the given `question_text` published the given
|
|
|
|
|
number of `days` offset to now (negative for questions published
|
|
|
|
|
in the past, positive for questions that have yet to be published).
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
time = timezone.now() + datetime.timedelta(days=days)
|
|
|
|
|
return Question.objects.create(question_text=question_text,
|
|
|
|
|
pub_date=time)
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
|
|
|
|
|
class QuestionViewTests(TestCase):
|
|
|
|
|
def test_index_view_with_no_questions(self):
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
If no questions exist, an appropriate message should be displayed.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
|
|
|
|
response = self.client.get(reverse('polls:index'))
|
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
|
self.assertContains(response, "No polls are available.")
|
2013-09-06 20:57:00 +02:00
|
|
|
|
self.assertQuerysetEqual(response.context['latest_question_list'], [])
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
def test_index_view_with_a_past_question(self):
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
Questions with a pub_date in the past should be displayed on the
|
|
|
|
|
index page
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
create_question(question_text="Past question.", days=-30)
|
2012-12-15 14:03:17 +01:00
|
|
|
|
response = self.client.get(reverse('polls:index'))
|
|
|
|
|
self.assertQuerysetEqual(
|
2013-09-06 20:57:00 +02:00
|
|
|
|
response.context['latest_question_list'],
|
|
|
|
|
['<Question: Past question.>']
|
2012-12-15 14:03:17 +01:00
|
|
|
|
)
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
def test_index_view_with_a_future_question(self):
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
Questions with a pub_date in the future should not be displayed on
|
|
|
|
|
the index page.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
create_question(question_text="Future question.", days=30)
|
2012-12-15 14:03:17 +01:00
|
|
|
|
response = self.client.get(reverse('polls:index'))
|
2013-09-06 20:57:00 +02:00
|
|
|
|
self.assertContains(response, "No polls are available.",
|
|
|
|
|
status_code=200)
|
|
|
|
|
self.assertQuerysetEqual(response.context['latest_question_list'], [])
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
def test_index_view_with_future_question_and_past_question(self):
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
Even if both past and future questions exist, only past questions
|
|
|
|
|
should be displayed.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
create_question(question_text="Past question.", days=-30)
|
|
|
|
|
create_question(question_text="Future question.", days=30)
|
2012-12-15 14:03:17 +01:00
|
|
|
|
response = self.client.get(reverse('polls:index'))
|
|
|
|
|
self.assertQuerysetEqual(
|
2013-09-06 20:57:00 +02:00
|
|
|
|
response.context['latest_question_list'],
|
|
|
|
|
['<Question: Past question.>']
|
2012-12-15 14:03:17 +01:00
|
|
|
|
)
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
def test_index_view_with_two_past_questions(self):
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
The questions index page may display multiple questions.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-10 15:54:26 +02:00
|
|
|
|
create_question(question_text="Past question 1.", days=-30)
|
2013-09-06 20:57:00 +02:00
|
|
|
|
create_question(question_text="Past question 2.", days=-5)
|
2012-12-15 14:03:17 +01:00
|
|
|
|
response = self.client.get(reverse('polls:index'))
|
|
|
|
|
self.assertQuerysetEqual(
|
2013-09-06 20:57:00 +02:00
|
|
|
|
response.context['latest_question_list'],
|
|
|
|
|
['<Question: Past question 2.>', '<Question: Past question 1.>']
|
2012-12-15 14:03:17 +01:00
|
|
|
|
)
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
|
2012-12-15 14:03:17 +01:00
|
|
|
|
Let's look at some of these more closely.
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
First is a question factory method, ``create_question``, to take some
|
|
|
|
|
repetition out of the process of creating questions.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
``test_index_view_with_no_questions`` doesn't create any questions, but checks
|
|
|
|
|
the message: "No polls are available." and verifies the ``latest_question_list``
|
|
|
|
|
is empty. Note that the :class:`django.test.TestCase` class provides some
|
2012-12-15 14:03:17 +01:00
|
|
|
|
additional assertion methods. In these examples, we use
|
2013-05-19 00:04:34 +02:00
|
|
|
|
:meth:`~django.test.SimpleTestCase.assertContains()` and
|
|
|
|
|
:meth:`~django.test.TransactionTestCase.assertQuerysetEqual()`.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
In ``test_index_view_with_a_past_question``, we create a question and verify that it
|
2012-12-15 14:03:17 +01:00
|
|
|
|
appears in the list.
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
In ``test_index_view_with_a_future_question``, we create a question with a
|
|
|
|
|
``pub_date`` in the future. The database is reset for each test method, so the
|
|
|
|
|
first question is no longer there, and so again the index shouldn't have any
|
|
|
|
|
questions in it.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
And so on. In effect, we are using the tests to tell a story of admin input
|
|
|
|
|
and user experience on the site, and checking that at every state and for every
|
|
|
|
|
new change in the state of the system, the expected results are published.
|
|
|
|
|
|
|
|
|
|
Testing the ``DetailView``
|
|
|
|
|
--------------------------
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
What we have works well; however, even though future questions don't appear in
|
|
|
|
|
the *index*, users can still reach them if they know or guess the right URL. So
|
|
|
|
|
we need to add a similar constraint to ``DetailView``::
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
|
2013-05-12 01:08:57 +02:00
|
|
|
|
class DetailView(generic.DetailView):
|
|
|
|
|
...
|
|
|
|
|
def get_queryset(self):
|
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
Excludes any questions that aren't published yet.
|
2013-05-12 01:08:57 +02:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
return Question.objects.filter(pub_date__lte=timezone.now())
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
And of course, we will add some tests, to check that a ``Question`` whose
|
2012-12-15 14:03:17 +01:00
|
|
|
|
``pub_date`` is in the past can be displayed, and that one with a ``pub_date``
|
|
|
|
|
in the future is not::
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
class QuestionIndexDetailTests(TestCase):
|
|
|
|
|
def test_detail_view_with_a_future_question(self):
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
The detail view of a question with a pub_date in the future should
|
2012-12-15 14:03:17 +01:00
|
|
|
|
return a 404 not found.
|
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
future_question = create_question(question_text='Future question.',
|
|
|
|
|
days=5)
|
|
|
|
|
response = self.client.get(reverse('polls:detail',
|
|
|
|
|
args=(future_question.id,)))
|
2012-12-15 14:03:17 +01:00
|
|
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
|
|
2013-09-06 20:57:00 +02:00
|
|
|
|
def test_detail_view_with_a_past_question(self):
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
The detail view of a question with a pub_date in the past should
|
|
|
|
|
display the question's text.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
"""
|
2013-09-06 20:57:00 +02:00
|
|
|
|
past_question = create_question(question_text='Past Question.',
|
|
|
|
|
days=-5)
|
|
|
|
|
response = self.client.get(reverse('polls:detail',
|
|
|
|
|
args=(past_question.id,)))
|
|
|
|
|
self.assertContains(response, past_question.question_text,
|
|
|
|
|
status_code=200)
|
|
|
|
|
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
Ideas for more tests
|
|
|
|
|
--------------------
|
|
|
|
|
|
2013-05-12 01:08:57 +02:00
|
|
|
|
We ought to add a similar ``get_queryset`` method to ``ResultsView`` and
|
|
|
|
|
create a new test class for that view. It'll be very similar to what we have
|
|
|
|
|
just created; in fact there will be a lot of repetition.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
We could also improve our application in other ways, adding tests along the
|
2013-09-06 20:57:00 +02:00
|
|
|
|
way. For example, it's silly that ``Questions`` can be published on the site
|
|
|
|
|
that have no ``Choices``. So, our views could check for this, and exclude such
|
|
|
|
|
``Questions``. Our tests would create a ``Question`` without ``Choices`` and
|
|
|
|
|
then test that it's not published, as well as create a similar ``Question``
|
|
|
|
|
*with* ``Choices``, and test that it *is* published.
|
|
|
|
|
|
|
|
|
|
Perhaps logged-in admin users should be allowed to see unpublished
|
|
|
|
|
``Questions``, but not ordinary visitors. Again: whatever needs to be added to
|
|
|
|
|
the software to accomplish this should be accompanied by a test, whether you
|
|
|
|
|
write the test first and then make the code pass the test, or work out the
|
|
|
|
|
logic in your code first and then write a test to prove it.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
|
|
|
|
At a certain point you are bound to look at your tests and wonder whether your
|
|
|
|
|
code is suffering from test bloat, which brings us to:
|
|
|
|
|
|
|
|
|
|
When testing, more is better
|
|
|
|
|
============================
|
|
|
|
|
|
|
|
|
|
It might seem that our tests are growing out of control. At this rate there will
|
|
|
|
|
soon be more code in our tests than in our application, and the repetition
|
|
|
|
|
is unaesthetic, compared to the elegant conciseness of the rest of our code.
|
|
|
|
|
|
|
|
|
|
**It doesn't matter**. Let them grow. For the most part, you can write a test
|
|
|
|
|
once and then forget about it. It will continue performing its useful function
|
|
|
|
|
as you continue to develop your program.
|
|
|
|
|
|
|
|
|
|
Sometimes tests will need to be updated. Suppose that we amend our views so that
|
2013-09-06 20:57:00 +02:00
|
|
|
|
only ``Questions`` with ``Choices`` are published. In that case, many of our
|
2012-12-15 14:03:17 +01:00
|
|
|
|
existing tests will fail - *telling us exactly which tests need to be amended to
|
|
|
|
|
bring them up to date*, so to that extent tests help look after themselves.
|
|
|
|
|
|
|
|
|
|
At worst, as you continue developing, you might find that you have some tests
|
|
|
|
|
that are now redundant. Even that's not a problem; in testing redundancy is
|
|
|
|
|
a *good* thing.
|
|
|
|
|
|
|
|
|
|
As long as your tests are sensibly arranged, they won't become unmanageable.
|
|
|
|
|
Good rules-of-thumb include having:
|
|
|
|
|
|
|
|
|
|
* a separate ``TestClass`` for each model or view
|
|
|
|
|
* a separate test method for each set of conditions you want to test
|
|
|
|
|
* test method names that describe their function
|
|
|
|
|
|
|
|
|
|
Further testing
|
|
|
|
|
===============
|
|
|
|
|
|
|
|
|
|
This tutorial only introduces some of the basics of testing. There's a great
|
|
|
|
|
deal more you can do, and a number of very useful tools at your disposal to
|
|
|
|
|
achieve some very clever things.
|
|
|
|
|
|
|
|
|
|
For example, while our tests here have covered some of the internal logic of a
|
|
|
|
|
model and the way our views publish information, you can use an "in-browser"
|
|
|
|
|
framework such as Selenium_ to test the way your HTML actually renders in a
|
|
|
|
|
browser. These tools allow you to check not just the behavior of your Django
|
|
|
|
|
code, but also, for example, of your JavaScript. It's quite something to see
|
|
|
|
|
the tests launch a browser, and start interacting with your site, as if a human
|
|
|
|
|
being were driving it! Django includes :class:`~django.test.LiveServerTestCase`
|
|
|
|
|
to facilitate integration with tools like Selenium.
|
|
|
|
|
|
|
|
|
|
If you have a complex application, you may want to run tests automatically
|
|
|
|
|
with every commit for the purposes of `continuous integration`_, so that
|
|
|
|
|
quality control is itself - at least partially - automated.
|
|
|
|
|
|
|
|
|
|
A good way to spot untested parts of your application is to check code
|
|
|
|
|
coverage. This also helps identify fragile or even dead code. If you can't test
|
|
|
|
|
a piece of code, it usually means that code should be refactored or removed.
|
|
|
|
|
Coverage will help to identify dead code. See
|
|
|
|
|
:ref:`topics-testing-code-coverage` for details.
|
|
|
|
|
|
2012-12-22 01:59:06 +01:00
|
|
|
|
:doc:`Testing Django applications </topics/testing/index>` has comprehensive
|
2012-12-15 14:03:17 +01:00
|
|
|
|
information about testing.
|
|
|
|
|
|
|
|
|
|
.. _Selenium: http://seleniumhq.org/
|
|
|
|
|
.. _continuous integration: http://en.wikipedia.org/wiki/Continuous_integration
|
|
|
|
|
|
|
|
|
|
What's next?
|
|
|
|
|
============
|
|
|
|
|
|
2013-03-04 12:05:11 +01:00
|
|
|
|
For full details on testing, see :doc:`Testing in Django
|
|
|
|
|
</topics/testing/index>`.
|
2012-12-15 14:03:17 +01:00
|
|
|
|
|
2013-03-04 12:05:11 +01:00
|
|
|
|
When you're comfortable with testing Django views, read
|
|
|
|
|
:doc:`part 6 of this tutorial</intro/tutorial06>` to learn about
|
|
|
|
|
static files management.
|