俗话说,人非圣贤,孰能无过。在堆代码的过程中,即便是老攻城狮,也会写下一些错误的内容。俗话又说,过而能改,善莫大焉。要改,首先要知道哪里存在错误,这便是我们要对投票应用进行测试的原因。
21.撰写第一个测试
在我们这个项目中,还真有一个bug存在。这个bug位于Question.was_published_recently() 方法中。当Question提交的日期是正确的,那没问题,但若提交的日期是错误的——比如日期是几天之后,问题就来了。
你可以在管理页面中增加一个投票,把日期设置在几天之后,你会发现你刚增加的投票被程序认为是“最近发布”的。
我们可以编写一段测试程序来界定问题。
编辑polls/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): """ 如果question的发布日期是在将来,那么was_published_recently()应该 返回一个False值。 """ time = timezone.now() + datetime.timedelta(days=30) future_question = Question(pub_date=time) self.assertEqual(future_question.was_published_recently(), False)
我们来运行一下测试,在Dos命令提示符下(注意,检查一下是否位于项目文件夹mysite下,,就象我们在Part1中所做的那样),输入:
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‘...
我们来看一下整个运行过程:
- 执行python manage.py test polls后,程序会自动检索polls应用下面的tests.py文档。
- 然后它会发现我们的测试类:QuestionMethodTests
- 程序会根据测试类创建一个临时数据库;
- 在test_was_published_recently_with_future_question中,程序会创建一个Question实例,它的发布日期在30天之后;
- 最后它使用了assertEqual() 方法,它发现was_published_recently() 返回的值是True,而实际上我们希望它返回的是False。
所以我们看到最终的结果是FAILED,说明我们的程序存在问题。
22.修复Bug
既然找到了问题所在,我们来修复它。
编辑polls/models.py 文件,作如下改动:
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‘...
这回正常了。
23.更多综合性的测试
有时项目中不止一个bug,下面,我们再编写两个测试。
编辑polls/tests.py 文件,在QuestionMethodTests类下添加下面的内容:
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)
现在,我们一共有三个测试来确认Question.was_published_recently() 在过去、最近、将来三个时间点上创建问题时返回正确的值。
当然。投票应用还只是一个非常简单的例子,相应的bug也不会很多。但在以后我们开发项目的过程中,我们会碰到一些复杂的应用,这时,测试就变得更加重要了。
24.测试视图
前面我们只是对应用的内部业务逻辑进行测试,接下来,我们要模拟用户操作,来测试我们的视图。
在Part4中,我们的Index视图使用了Django通用视图中的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]
我们需要修正get_queryset这个方法,让它在提取数据时检查发布时间,与当前时间进行比对。我们编辑polls/views.py,在文件头部先加入:
polls/views.py:
from django.utils import timezone
然后修正get_queryset方法 :
polls/views.py:
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]
Question.objects.filter(pub_date__lte=timezone.now())可确保返回的结果集中的Question对象的发布日期早于或等于当前的时间。
现在我们来测试一下这个新的视图。
编辑polls/tests.py,先在文件头部加入:
polls/tests.py:
from django.core.urlresolvers import reverse
然后再加上以下内容:
polls/tests.py:
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‘], []) 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.>‘] )
我们来看一下这部分代码:
- 首先我们使用了一个叫create_question的函数,它是用来快速创建问题的,因为随后的测试中都会用到,所以有这个函数,可减少一些重复劳动。
- test_index_view_with_no_questions不创建任何问题,只是检查当没有任何投票问题的时候,首页是否能返回“No polls are available.”这个信息,同时检查latest_question_list是不是空的。
- 在test_index_view_with_a_past_question中,我们创建了一个问题,把它的发布时间设在了30天前,然后检查它是不是出现在首页的列表中;
- 在test_index_view_with_a_future_question中,我们创建了一个问题,把它的发布时间设在了30天后,这里的每一个测试方法在执行的时候,数据库都会重置。所以我们在上一步测试中创建的那个问题不再存在,这样,首页的列表就应该是空的;
即使我们在首页视图中不再显示那些发布在将来时段的问题,但还会有用户通过合适的链接来访问到这些内容。这就意味着,我们要调整内容页的视图。
编辑polls/views.py,在DetailView中加入get_queryset方法:
polls/views.py:
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,加入下列内容:
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)
我们来简单分析一下这段测试:
- 在test_detail_view_with_a_future_question中,我们创建了一个问题,把它的发布时间设置在5天后,然后模拟用户去访问,如果我们新的DetailView起作用的话,这个链接应该是空的,换句话说,访问这个链接时,用户会得到一个404的状态码。
- 在test_detail_view_with_a_past_question中,我们创建了一个问题,把它的发布时间设置在5天前,同样模拟用户去访问,这种情况下,用户会得到的状态码应该是200,也就是说,链接是有效的。
我们还可以就更多的问题进行测试,同时根据测试优化我们的应用。
举个例子,用户在使用这个应用的过程中,发布投票时没有带任何的投票项,此时,我们的视图需要具备相应的检测功能,来防止这类事情的发生。我们可以编写一个测试,创建一个问题,让它不再任何投票项,通过这样的测试来界定问题,并根据测试结果对视图进行调整。
在测试中,我们奉行一个理念:测试越多越好。测试越多,说明我们的应用越可靠。或许有一天,我们的测试代码的数量甚至超过了正式代码,不必在意这些。测试只会让我们的代码愈来愈成熟。
【未完待续】
本文版权归舍得学苑所有,欢迎转载,转载请注明作者和出处。谢谢!
作者:舍得
首发:舍得学苑@博客园