参考:https://docs.djangoproject.com/en/1.9/intro/tutorial05/
上一个测试我们了解到了如何测试一个方法是否如我们预期的工作方式一致,接下来我们来看看其他功能。django提供了一个测试客户端,可以在view层面模拟用户的操作。我们可以在tests.py或者shell中使用它。
让我们从shell开始。在shell测试中我们需要一些在tests.py中不必使用的操作。首先,我们要来设置运行环境:
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
setup_test_environment() 方法会安装一些模版渲染器,使得我们可以检查responses的一些额外属性,比如respone.context之类。需要注意的是,这个方法并不会安装test数据库,所以以下操作也许会因为个人之前question的不同而导致输出不同。
设置完环境后,我们需要来导入Client类:
>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = 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
b'\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
>>> 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
b'\n\n\n <ul>\n \n <li><a href="/polls/1/">Who is your favorite Beatle?</a></li>\n \n </ul>\n\n'
>>> # 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?>]
现在我们的polls/index显示的question包括了那些还未发布的(pub_date在future的),让我们来改进它。
在之前的代码中,我们基于generic.ListView对index定义如下:
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]
显然,我们需要更新get_queryset()方法,检查question的pubdate。如下:
def get_queryset(self):
''' return the last five published questions.'''
return Question.objects.filter(pub_date__lte=timezone.now()).order_by('-pub_date')[:5]
.filter(pub_date__lte=timezone.now())是一个过滤器,返回pub_date小于等于timezone.now()的list。
下面让我们来测试它。如果不使用测试用例系统,你需要手动建立Question,运行服务器,检查结果是否如你所愿。这显然太麻烦了。以下我们使用tests.py来进行测试,和用shell方法类似。
首先我们设置一个快速建立Question的方法,以便在后续代码中调用:
def create_question(question_text, days):
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)
接下来编写测试用例。记得导入reverse()先:
class QuestionViewTests(TestCase):
def test_index_view_with_no_question(self):
"""
If no questions exist, an appropriate message should be displayed.
"""
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
#print(response.context)
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)
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.>']
)
其中:
- test_index_view_with_no_question测试了空question时候的返回对象的['latest_question_list']值和status_code。
- test_index_view_with_a_past_question测试了单个正常Question时候的显示。
- test_index_view_with_a_future_question测试了pub_date在未来时候的question显示。每一个方法运行时,数据库都会重置,所以之前方法保存的question不会在其中出现。
关于asserQuerysetEqual(),请参考:https://docs.djangoproject.com/en/1.9/topics/testing/tools/#django.test.TransactionTestCase.assertQuerysetEqual
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
OK,我们在tests.py中继续添加一些例子,来保证DetailView的运行:
class QuestionDetailTests(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,)))
#print(response.context)
self.assertContains(response, past_question.question_text, status_code=200)
类似的,我们也可以继续添加对于ResultsView的测试。例如:我们可以让没有choice的Question不被展示;或者可以让没有choice的Question只被管理员用户可见,不被其他用户可见,等等。总之,不论任何你想要的功能,你都应该为其增添测试,不管你是先有测试用例再完成功能,还是先有功能再添加测试用例。
经过这些测试,你也许会发现,这些测试用例代码越来越大,甚至超过你的apps的代码,但是没关系,我们有一个原则:
测试用例越多越好(When testing, more is better)
虽然这些测试代码量可能会疯长,但是没关系,let them grow。对于大多数情况,你只需要编写一次测试用例,然后就可以把他们忘记了;有些时候你需要更新这些测试用例,但是挂了的测试用例仍然可以指导我们哪里需要修改,直至被修复;有些时候,随着开发工作的进展,有的测试用例是冗余的,但是没关系,冗余测试仍然是好的事情。
只要你的测试用例被很好的管理,他们不会失去控制。你应该遵守如下比较有用的规则:
- 对于每个model或者view,编写独立的测试类
- 对于每一种测试情况,编写不同的测试方法
- 测试方法名字应该对测试内容有说明含义