Writing Your First Django App 05
Writing Your First Django App 05
Writing Your First Django App 05
This tutorial begins where Tutorial 4 left off. Weve built a Web-poll application, and well now create some automated tests for it.
Since things in the future are not recent, this is clearly wrong.
Create a test to expose the bug
What weve just done in the shell to test for the problem is exactly what we can do in an automated test, so lets turn that into an automated test.
A conventional place for an applications tests is in the applications tests.py file; the testing system will automatically find tests in any file whose name begins with test.
Put the following in the tests.py file in the polls application:
polls/tests.py
import datetime
from django.utils import timezone
from django.test import TestCase
from .models import Question
class QuestionMethodTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently() should return False for questions whose
pub_date is in the future.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertEqual(future_question.was_published_recently(), False)
What we have done here is created a django.test.TestCase subclass with a method that creates a Question instance with a pub_date in the future. We then check the output
of was_published_recently() - which ought to be False.
Running tests
In the terminal, we can run our test:
$ python manage.py test polls
and youll see something like:
Creating test database for alias 'default'...
F
======================================================================
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 django.test.TestCase class
it created a special database for the purpose of testing
it looked for test methods - ones whose names begin with test
in test_was_published_recently_with_future_question it created a Question instance whose pub_datefield is 30 days in the future
... and using the assertEqual() method, it discovered that its was_published_recently() returns True, though we wanted it to return False
The test informs us which test failed and even the line on which the failure occurred.
Fixing the bug
We already know what the problem is: Question.was_published_recently() should return False if its pub_date is in the future. Amend the method in models.py, so that it
will only return True if the date is also in the past:
polls/models.py
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
and run the test again:
Creating test database for alias 'default'...
.
---------------------------------------------------------------------Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
After identifying a bug, we wrote a test that exposes it and corrected the bug in the code so our test passes.
Many other things might go wrong with our application in the future, but we can be sure that we wont 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 were 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:
polls/tests.py
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() should return False for questions whose
pub_date is older than 1 day.
"""
time = timezone.now() - datetime.timedelta(days=30)
old_question = Question(pub_date=time)
self.assertEqual(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() should return True for questions whose
pub_date is within the last day.
"""
time = timezone.now() - datetime.timedelta(hours=1)
recent_question = Question(pub_date=time)
self.assertEqual(recent_question.was_published_recently(), True)
And now we have three tests that confirm that Question.was_published_recently() returns sensible values for past, recent, and future questions.
Again, polls is a simple application, but however complex it grows in the future and whatever other code it interacts with, we now have some guarantee that the method we have
written tests for will behave in expected ways.
Test a view
The polls application is fairly undiscriminating: it will publish any question, including ones whose pub_date field lies in the future. We should improve this. Setting a pub_date in the
future should mean that the Question is published at that moment, but invisible until then.
\n
</ul>\n\n'
The list of polls shows polls that arent published yet (i.e. those that have a pub_date in the future). Lets fix that.
In Tutorial 4 we introduced a class-based view, based on ListView:
polls/views.py
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by('-pub_date')[:5]
We need to amend the get_queryset() method and change it so that it also checks the date by comparing it withtimezone.now(). First we need to add an import:
polls/views.py
from django.utils import timezone
and then we must amend the get_queryset method like so:
polls/views.py
def get_queryset(self):
"""
Return the last five published questions (not including those set to be
class QuestionViewTests(TestCase):
def test_index_view_with_no_questions(self):
"""
If no questions exist, an appropriate message should be displayed.
"""
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available.")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_index_view_with_a_past_question(self):
"""
Questions with a pub_date in the past should be displayed on the
index page.
"""
create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past question.>']
)
def test_index_view_with_a_future_question(self):
"""
Questions with a pub_date in the future should not be displayed on
the index page.
"""
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertContains(response, "No polls are available.",
status_code=200)
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_index_view_with_future_question_and_past_question(self):
"""
Even if both past and future questions exist, only past questions
should be displayed.
"""
create_question(question_text="Past question.", days=-30)
10
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
And of course, we will add some tests, to check that a Question whose pub_date is in the past can be displayed, and that one with a pub_date in the future is not:
polls/tests.py
class QuestionIndexDetailTests(TestCase):
def test_detail_view_with_a_future_question(self):
"""
The detail view of a question with a pub_date in the future should
return a 404 not found.
"""
future_question = create_question(question_text='Future question.',
days=5)
response = self.client.get(reverse('polls:detail',
args=(future_question.id,)))
self.assertEqual(response.status_code, 404)
def test_detail_view_with_a_past_question(self):
"""
The detail view of a question with a pub_date in the past should
display the question's text.
"""
past_question = create_question(question_text='Past Question.',
days=-5)
response = self.client.get(reverse('polls:detail',
args=(past_question.id,)))
self.assertContains(response, past_question.question_text,
status_code=200)
Ideas for more tests
We ought to add a similar get_queryset method to ResultsView and create a new test class for that view. Itll be very similar to what we have just created; in fact there will be a lot
of repetition.
11
We could also improve our application in other ways, adding tests along the way. For example, its 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 its not published, as well as create a
similar Question withChoices, and test that it is published.
Perhaps logged-in admin users should be allowed to see unpublished Questions, but not ordinary visitors. Again: whatever needs to be added to the software to accomplish this
should be accompanied by a test, whether you write the test first and then make the code pass the test, or work out the logic in your code first and then write a test to prove it.
At a certain point you are bound to look at your tests and wonder whether your code is suffering from test bloat, which brings us to:
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 doesnt matter. Let them grow. For the most part, you can write a test once and then forget about it. It will continue performing its useful function as you continue to develop your
program.
Sometimes tests will need to be updated. Suppose that we amend our views so that only Questions with Choices are published. In that case, many of our existing tests will fail telling us exactly which tests need to be amended to bring them up to date, so to that extent tests help look after themselves.
At worst, as you continue developing, you might find that you have some tests that are now redundant. Even thats not a problem; in testing redundancy is a good thing.
As long as your tests are sensibly arranged, they wont 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. Theres 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. Its 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 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 cant test a piece of code, it usually means
that code should be refactored or removed. Coverage will help to identify dead code. See Integration with coverage.py for details.
Testing in Django has comprehensive information about testing.
12