Django 2.0 学习(08):Django 自动化测试

编写我们的第一个测试

确定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
  • 对于每个测试条件编写独立的测试方法;
  • 可以通过测试方法的名字理解其功能;
上一篇:十、Python练习----基础搭建飞机大战


下一篇:MWeb 1.6 发布!Dark Mode、全文搜寻、发布到Wordpress、Evernote 等支持更新、编辑/预览视图模式等