七、自动化测试
在开发中,我们都需要测试代码的正确性。前面的例子都是我们写好代码后,运行开发服务器,在浏览器上自己点击测试,看写的代码是否正常,但是这样做很麻烦,因为以后如果有改动,可能会影响以前本来正常的功能,这样以前的功能又得测试一遍,非常不方便,Django中有完善的单元测试,我们可以对开发的每一个功能进行单元测试,这样只要运行一个命令 python manage.py test,就可以测试功能是否正常。
测试就是检查代码是否按照自己的预期那样运行。自动化测试的不同之处在于测试工作是由系统为您完成的。您只需创建一组测试,然后在对应用程序进行更改时,可以检查代码是否仍然按照您的初始设计工作,而不必执行耗时的手动测试。
测试驱动开发: 有时候,我们知道自己需要的功能(结果),并不知道代码如何书写,这时候就可以利用测试驱动开发(Test Driven Development),先写出我们期待得到的结果(把测试代码先写出来),再去完善代码,直到不报错,我们就完成了。
1、shell测试
在我们的投票应用程序中,有一个小错误,在polls/models.py中,Question.was_published_recently() 函数是用于判断是否是过去最近一天内发表的,当实际上,并没有完全考虑到。对于在将来发表的,还是返回True。我们可以在项目环境终端shell上测试,代码如下:
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> # create a Question instance with pub_date 30 days in the future
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # was it published recently?
>>> future_question.was_published_recently()
True
2、开始自动化测试
我们在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):
"""
was_published_recently() returns False for questions whose pub_date
is in the future.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
在cmd中,进入到项目目录,输入下面语句开始测试:
python manage.py test polls
//python manage.py test polls.tests.QuestionModelTests
可以看到:
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 "/path/to/mysite/polls/tests.py", line 16, 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'...
可以看到是File “/path/to/mysite/polls/tests.py”, line 16, in test_was_published_recently_with_future_question这里出了问题,查看上下文,发现return值那里判断不完整,修改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'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
我们可以进行更多的测试,在polls/tests.py输入以下代码进行测试:
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is older than 1 day.
"""
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):
"""
was_published_recently() returns True for questions whose pub_date
is within the last day.
"""
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)
3、使用测试客户端
Django提供了一个测试Client模拟用户在视图级别与代码进行交互。我们可以在tests.py或者shell中使用它。我们先在shell中使用它,在那里我们需要做一些在tests.py中不必要的事情。首先是建立测试环境,代码如下:
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
接着我们需要导入Client类,但在tests.py中我们使用django.test.TestCase替代,代码如下:
>>> from django.test import Client
>>> #为客户端创建一个实例供我们使用
>>> client = Client()
开始测试:
>>> # 从'/'获取响应
>>> response = client.get('/')
Not Found: /
>>> # 我们应该从这个地址得到一个404; 如果你看到一个
>>> # "Invalid HTTP_HOST header" 错误和一个400响应,你可能
>>> # 省略了前面描述的setup_test_environment()调用。
>>> response.status_code
404
>>> # 另一方面,我们应该期望找到'/polls/'
>>> # 我们将使用'reverse()'
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
b'\n <ul>\n \n <li><a href="/polls/1/">What's up?</a></li>\n \n </ul>\n\n'
>>> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>
可以发现,这里跟前面一样,都会获得将来发表的questions。
4、测试ListView
修改polls/views.py的IndexView,代码如下:
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]
根据上面的shell会话创建一个测试,在polls/tests.py增加代码,如下:
from django.urls import reverse
def create_question(question_text, days):
"""
用给定的‘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.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_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.>']
)
5、测试DetailView
同样,先修改polls/views.py的DetailView,代码如下:
class DetailView(generic.DetailView):
...
def get_queryset(self):
"""
排除尚未发布的问题。
"""
return Question.objects.filter(pub_date__lte=timezone.now())
在polls/tests.py增加代码,如下:
class QuestionDetailViewTests(TestCase):
def test_future_question(self):
"""
如果问题尚未发布,返回404未找到。
"""
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):
"""
如果是过去发布的问题,显示question_text。
"""
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)
想了解更多关于自动化测试,请查阅Testing in Django。
参考资料