编写我们的第一个测试
确定bug
幸运的是,在polls应用中存在一个小小的bug急需修复:无论Question的发布日期是最近(最后)的日期,还是将来很多天的日期,Question.was_published_recently()方法都会返回True。使用下面的代码对其进行验证:
>>> import datetime
>>>
>>> from django.utils import timezone
>>> from polls.models import Question
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> future_question.was_published_recently()
True
然而,将来的事情不可能是最近(最后)的,这是明显的错误。
创建测试显示bug
我们刚刚在shell中完成的测试问题正是我们在自动化测试中要做的,因此让我们转入自动化测试。按照惯例,对应用的测试就是在应用的tests.py文件中,测试系统会自动查找以test开始的任意文件。在polls应用中编辑tests.py文件,代码如下:
import datetime
from django.utils import timezone
from django.test import TestCase
from .models import Question
class QuestionModelTests(TestCase):
def test_was_published_recently_with_future_question(self):
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
这里我们所做的是创建一个django.test.TestCase子类,该子类只有一个方法:使用pub_date创建一个Question实例。检查was_published_recently()的输出,应该是False。
运行测试
在终端中,运行测试代码:
python manage.py test polls
我们将会看到下面的输出内容:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "E:\mysite\polls\tests.py", line 12, in test_was_published_recently_with_future_question
self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
我们来分析下,发生了什么:
- python manage.py test polls命令在polls应用中查找测试;
- 查找到django.test.TestCase子类;
- 为目标测试创建一个专门的数据库;
- 查找一个名字以test开始的方法;
- 在test_was_published_recently_with_future_question方法中,创建一个Question实例,该实例的pub_date字段是将来的30天;
- 最后,assertIs()方法发现was_published_recently()返回True,而我们想要的返回是False;
测试通知我们哪个测试失败了,并且失败产生在代码的哪行。
修复bug
我们已经知道问题是:Question.was_published_recently()在其参数pub_date是将来的日期时会返回False。修改models.py文件中的方法,让它在日期是过去的情况下也能返回True,其代码如下:
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
再次运行测试命令,会看到如下所示:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Destroying test database for alias 'default'...
更全面的测试
在这里,我们可以进一步约束was_published_recently()方法;实际上,在修复bug时引进其他bug是非常尴尬的事情。在相同的类中,我们增加两个方法来全面的测试方法的行为,编辑文件polls/tests.py代码如下:
import datetime
from django.utils import timezone
from django.test import TestCase
from .models import Question
class QuestionModelTests(TestCase):
def test_was_published_recently_with_future_question(self):
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
def test_was_published_recently_with_old_question(self):
time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_question = Question(pub_date=time)
self.assertIs(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
recent_question = Question(pub_date=time)
self.assertIs(recent_question.was_published_recently(), True)
现在有三个测试方法来证实Question.was_published_recently()对过去、现在和将来的返回值。
测试视图
Django测试客户端
Django提供了一个测试客户端,用来模仿用户在视图级别与我们的代码互动。我们可以在test.py中使用它,也可以在shell中使用。我们将在shell中来进行测试,首先启动shell并在其中设置测试环境:
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
setup_test_environment()安装一个模板渲染器,它可以检查响应的某些附加属性,比如:response.context;否则附加属性将不可用,但是该方法不会设置测试数据库。
接下来,我们需要导入测试客户端类:
>>> from django.test import Client
>>> client = Client() // 创建一个client实例,供我们接下来使用
准备好之后,我们就可以利用client做些测试工作:
>>> response = client.get('/')
Not Found: /
>>> response.status_code
404
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
b'\n <ul>\n \n <li>\n <a href="/polls/1/">What is up old?</a>\n
</li>\n \n </ul>\n'
>>> response.context['latest_question_list']
<QuerySet [<Question: What is up old?>]>
改进视图函数
打开polls/views.py文件,添加代码如下:
from django.utils import timezone
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
return Question.objects.filter(pub_date__lte=timezone.now()).order_by('-pub_date')[:5]
Question.objects.filter(pub_date__lte=timezone.now()) 返回一个Questions查询集合,该集合是pub_date小于或等于timezone.now的数据。
现在我们基于test.py文件进行新的测试,打开polls/test.py文件,添加如下代码:
from django.urls import reverse
def create_question(question_text, days):
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)
class QuestionIndexViewTests(TestCase):
def test_no_questions(self):
response = self.client.generic(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_past_question(self):
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_future_question(self):
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertContains(response, "No polls are available.")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_future_question_and_past_question(self):
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_two_past_questions(self):
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_no_question没有创建任何问题,只是检查信息No polls are available"",并且确认latest_question_list是否为空。django.test.TestCase类提供了额外的断言方法,上面的例子中就用到asserContains()和assertQuerysetEqual()这两个方法;
- 在test_past_question中,我们创建了一个问题并且确认该问题会出现在问题列表中;
- 在test_future_question中,我们创建了一个发布日期是将来的问题。对于每个测试方法,数据库都会被重置,所以第一个问题不会存在在数据库中,并且在index中不会由任何问题;
测试DetailView
我们做的已经很好了,尽管将来的问题没有显示在index页面中,但是如果用户知道正确的URL或者猜到它们,用户依然能够访问到它们。所以,我们需要对DetailView视图添加相似的约束,依然打开polls/views.py文件,修改代码如下:
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
def get_queryset(self):
return Question.objects.filter(pub_date__lte=timezone.now())
同理,我们将会添加一些测试,去检查每个问题:如果发布日期是过去的,就显示;如果发布日期是将来的,就不显示;打开polls/tests.py文件,添加下面代码:
class QuestionDetailViewTests(TestCase):
def test_future_question(self):
future_question = create_question(question_text='Future question.', days=5)
url = reverse('polls:detail', args=(future_question.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_past_question(self):
past_question = create_question(question_text='Past Question.', days=-5)
url = reverse('polls:detail', args=(past_question.id,))
response = self.client.get(url)
self.assertContains(response, past_question.question_text)
好的测试规则应该遵循:
- 对于每个模型或者视图,编写独立的(测试类)TestClass;
- 对于每个测试条件编写独立的测试方法;
- 可以通过测试方法的名字理解其功能;