writing_first_django_app_part5
Automated Testing
解决问题一个很好的方法是首先描述这个问题,然后写出代码来实现它。但是很多人习惯先写好代码然后再进行调试找问题。也许在写代码之前先写好一些测试会是更好的选择。
A little bug
前面在我们的Question.was_published_recently()函数中有一个小问题,如果它是一天之内创建的则会返回True,但是如果pub_date是未来的时间也会返回True
通常应用的测试会放到tests.py文件中,现在在polls/tests.py里添加测试:
import datetime from django.utils import timezone from django.test import TestCase from polls.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)
这个测试类是继承于TestCase类,它创建了一个将来时间的Question,然后self.assertEqual()函数是测试函数的返回结果,正确应该返回False
运行这个测试:
$ python manage.py test polls
得到如下结果:
Creating test database for alias 'default'... F ====================================================================== FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests) ---------------------------------------------------------------------- Traceback (most recent call last): 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) AssertionError: True != False ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1) Destroying test database for alias 'default'...
首先test polls会在polls应用下查找测试,然后找到TestCase类的子类,就会创建一个特定的数据库来进行测试。测试类中测试的函数会以'test'开头,即上面的test_was published_...,最后就是运行assertEqual函数看结果是否与预想的一致,上面的结果是FAILED
修正polls/models.py:
def was_published_recently(self): now = timezone.now() return now - datetime.timedelta(days=1) <= self.pub_date <= now
再次运行测试:
Creating test database for alias 'default'... . ---------------------------------------------------------------------- Ran 1 test in 0.001s OK Destroying test database for alias 'default'...
测试异常的情况之后,还要加上一个正确的例子,函数应该返回True:
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)
Test a view
Django提供一个测试Client来模拟一个用户在view测层面与代码进行交互。可以通过tests.py或者shell来进行。
用shell来测试:
# 设置测试环境 >>> from django.test.utils import setup_test_environment >>> setup_test_environment() # 建立一个client来进行测试 >>> from django.test import Client >>> # create an instance of the client for our use >>> client = Client() # 一些测试用例 >>> # 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 hardcoded URL >>> from django.core.urlresolvers import reverse >>> response = client.get(reverse('polls:index')) >>> response.status_code 200 >>> response.content ' <p>No polls are available.</p> ' >>> # 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 >>> from polls.models import Question >>> from django.utils import timezone >>> # create a Question and save it >>> q = Question(question_text="Who is your favorite Beatle?", pub_date=timezone.now()) >>> q.save() >>> # check the response once again >>> response = client.get('/polls/') >>> response.content ' <ul> <li><a href="/polls/1/">Who is your favorite Beatle?</a></li> </ul> ' >>> # If the following doesn't work, you probably omitted the call to >>> # setup_test_environment() described above >>> response.context['latest_question_list'] [<Question: Who is your favorite Beatle?>]
前面我们使用了generic.ListView来创建view,如下:
# 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]
最后返回Question并没有对时间在现在之后的进行筛选,我们作出修改:
from django.utils import timezone def get_queryset(self): """ Return the last five published questions (not including those set to be published in the future). """ return Question.objects.filter( pub_date__lte=timezone.now() ).order_by('-pub_date')[:5]
filter(pub_date__lte=timezone.now()返回时间早于或者等于now的Questions
Testing the view
在tests.py中添加对index的测试:
from django.core.urlresolvers import reverse def create_question(question_text, days): """ 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). """ time = timezone.now() + datetime.timedelta(days=days) return Question.objects.create(question_text=question_text, pub_date=time) 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'], []) 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) create_question(question_text="Future 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_two_past_questions(self): """ The questions index page may display multiple questions. """ create_question(question_text="Past question 1.", days=-30) create_question(question_text="Past question 2.", days=-5) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], ['<Question: Past question 2.>', '<Question: Past question 1.>'] )
首先为了方便在测试的时候创建question,先定义函数create_question. 下面几个函数分别对几个情况进行测试,注意函数命名要体现测试的内容。
Test DetailView
class DetailView(generic.DetailView): ... def get_queryset(self): """ Excludes any questions that aren't published yet. """ return Question.objects.filter(pub_date__lte=timezone.now())
# 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)
上面就是一些测试的例子,测试代码可能看起来很多,但其实它们都是非常有用的。When testing, more is better