介绍自动化测试¶
什么是自动化测试?¶
测试是检查代码操作的简单例程。
测试在不同级别进行。某些测试可能适用于一个微小的细节(特定模型方法是否按预期返回值?)而其他测试则检查软件的整体操作(网站上的一系列用户输入是否会产生所需的结果?)。这与您在教程2中之前所做的测试类型没有什么不同,使用它 shell来检查方法的行为,或者运行应用程序并输入数据来检查它的行为方式。
自动化测试的不同之处在于测试工作是由系统完成的。您只需创建一组测试,然后在对应用程序进行更改时,可以检查代码是否仍按预期工作,而无需执行耗时的手动测试。
为什么需要创建测试¶
那么为什么要创建测试,为什么现在呢?
您可能觉得自己已经足够了解Python / Django,并且还有另外一些需要学习和做的事情可能看起来势不可挡,也许是不必要的。毕竟,我们的民意调查申请现在非常愉快; 经历创建自动化测试的麻烦不会让它更好地工作。如果创建民意调查应用程序是您将要做的最后一点Django编程,那么,您不需要知道如何创建自动化测试。但是,如果情况并非如此,那么现在是学习的绝佳时机。
测试会节省你的时间¶
在某一点上,“检查它似乎有用”将是一个令人满意的测试。在更复杂的应用程序中,组件之间可能会有许多复杂的交互。
任何这些组件的更改都可能会对应用程序的行为产生意外后果。检查它是否“似乎工作”可能意味着通过代码的功能运行20种不同的测试数据变体,以确保您没有破坏某些东西 - 不能充分利用您的时间。
当自动化测试可以在几秒钟内为您完成此操作时尤其如此。如果出现问题,测试还将有助于识别导致意外行为的代码。
有时,如果您知道自己的代码工作正常,那么将自己从富有成效的创造性编程工作中剔除,以面对编写测试的无趣和令人兴奋的业务,这似乎是件苦差事。
但是,编写测试的任务比花费数小时手动测试应用程序或尝试确定新引入的问题的原因要多得多。
测试不只是识别问题,而是阻止它们¶
将测试仅仅视为发展的消极方面是错误的。
如果没有测试,应用程序的目的或预期行为可能会相当不透明。即使它是你自己的代码,你有时会发现自己在试图找出它究竟在做什么。
测试改变了; 他们从内部点亮你的代码,当出现问题时,他们将光线集中在出错的部分 - 即使你甚至没有意识到它出了问题。
测试使您的代码更具吸引力¶
您可能已经创建了一个出色的软件,但您会发现很多其他开发人员只会拒绝查看它,因为它缺少测试; 没有测试,他们就不会相信它。Django最初的开发人员之一Jacob Kaplan-Moss说:“没有测试的代码被设计破坏了。”
其他开发人员希望在认真对待之前在软件中看到测试是您开始编写测试的另一个原因。
测试帮助团队一起工作¶
以前的观点是从维护应用程序的单个开发人员的角度编写的。复杂的应用程序将由团队维护。测试保证同事不会无意中破坏您的代码(并且您不会在不知情的情况下破坏他们的代码)。如果你想以Django程序员谋生,你必须善于编写测试!
基本测试策略¶
有很多方法可以用来编写测试。
一些程序员遵循一门名为“ 测试驱动开发 ” 的学科; 他们实际上在编写代码之前编写测试。这可能看似违反直觉,但实际上它与大多数人经常会做的类似:他们描述一个问题,然后创建一些代码来解决它。测试驱动的开发只是在Python测试用例中形式化了问题。
更常见的情况是,测试的新手将创建一些代码,然后决定它应该进行一些测试。也许最早写一些测试会好一些,但是从来没有太晚开始。
有时候很难弄清楚从哪里开始编写测试。如果您已经编写了几千行Python,那么选择要测试的东西可能并不容易。在这种情况下,无论是在添加新功能还是修复错误时,下次进行更改时编写第一个测试都很有成效。
所以让我们马上做。
写第一次测试¶
我们发现了一个错误¶
幸运的是,在一个小错误polls的应用为我们立即进行修复:该Question.was_published_recently()方法返回True,如果Question是最后一天(这是正确的)内发布,而且如果Question的pub_date领域是未来(这当然不是) 。
通过使用shell以检查日期位于以后的问题上的方法来确认错误:
/
$ python manage.py shell
import datetime
from django.utils import timezone
from polls.models import Questioncreate 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
由于未来的事情不是“最近的”,这显然是错误的。
创建测试以揭示错误¶
我们在shell测试问题时所做的正是我们在自动化测试中可以做的,所以让我们把它变成一个自动测试。
应用程序测试的常规位置在应用程序的 tests.py文件中; 测试系统将自动在名称以…开头的任何文件中查找测试test。
将以下内容放在应用程序的tests.py文件中polls:
民调/ tests.py中¶
import datetime
from django.test import TestCase
from django.utils import timezone
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)
在这里,我们创建了一个django.test.TestCase子类,其中包含一个创建未来Question实例的方法pub_date。然后我们检查输出was_published_recently()- 哪个应该是假的。
运行测试¶
在终端中,我们可以运行我们的测试:
/
$ 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 “/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’…
发生了什么事:
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
测试通知我们哪个测试失败,甚至是发生故障的线路。
修复错误¶
我们已经知道问题是什么:如果它是将来Question.was_published_recently()应该返回。修改方法 ,以便只有在日期也是过去时它才会返回:Falsepub_datemodels.pyTrue
民调/ 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’…
在识别出错误之后,我们编写了一个公开它的测试并更正了代码中的错误,以便我们的测试通过。
我们的应用程序将来可能会出现许多其他问题,但我们可以肯定,我们不会无意中重新引入此错误,因为只需运行测试就会立即向我们发出警告。我们可以认为应用程序的这一小部分永远安全地固定下来。
更全面的测试¶
当我们在这里时,我们可以进一步确定was_published_recently() 方法; 事实上,如果修复我们引入另一个错误的一个错误,那将是非常尴尬的。
在同一个类中再添加两个测试方法,以更全面地测试该方法的行为:
民调/ 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)
现在,我们有三项测试证实可以Question.was_published_recently() 为过去,近期和未来的问题返回合理的价值。
同样,它polls是一个简单的应用程序,但无论它在将来如何复杂,以及它与之交互的其他代码,我们现在都能保证我们编写的测试方法将以预期的方式运行。
测试视图¶
民意调查申请是相当无差别的:它将发布任何问题,包括其pub_date领域在未来的问题。我们应该改善这一点。pub_date在将来设置a 应该意味着该问题在那个时刻发布,但在此之前是不可见的。
对视图的测试¶
当我们修复上面的错误时,我们首先编写测试,然后编写代码来修复它。事实上,这是测试驱动开发的一个简单示例,但我们的工作顺序并不重要。
在我们的第一次测试中,我们密切关注代码的内部行为。对于此测试,我们希望检查用户通过Web浏览器体验的行为。
在我们尝试解决任何问题之前,让我们来看看我们可以使用的工具。
Django测试客户端¶
Django提供了一个测试Client来模拟用户在视图级别与代码交互。我们可以在它中使用它tests.py 甚至在它中使用它shell。
我们将重新开始shell,我们需要做一些不必要的事情tests.py。首先是在以下位置设置测试环境shell:
/
$ python manage.py shell
from django.test.utils import setup_test_environment
setup_test_environment()
setup_test_environment()安装模板渲染器,这将允许我们检查响应上的一些其他属性 response.context,否则将无法使用。请注意,此方法不会设置测试数据库,因此将针对现有数据库运行以下内容,并且输出可能会略有不同,具体取决于您已创建的问题。如果你的TIME_ZONE输入settings.py不正确,你可能会得到意想不到的结果 。如果您不记得先提前设置,请在继续之前进行检查。
接下来我们需要导入测试客户端类(稍后tests.py我们将使用django.test.TestCase该类,它带有自己的客户端,因此不需要):
from django.test import Client
create an instance of the client for our use
client = Client()
准备好后,我们可以要求客户为我们做一些工作:
get a response from ‘/’
response = client.get(’/’)
Not Found: /we should expect a 404 from that address; if you instead see an
“Invalid HTTP_HOST header” error and a 400 response, you probably
omitted the setup_test_environment() call described earlier.
response.status_code
404on the other hand we should expect to find something at ‘/polls/’
we’ll use ‘reverse()’ rather than a hardcoded URL
from django.urls import reverse
response = client.get(reverse(‘polls:index’))
response.status_code
200response.content
b’\n\n\n’
- \n \n
- What’s up?
- \n \n
response.context[‘latest_question_list’]
<QuerySet [<Question: What’s up?>]>
改善我们的观点¶
民意调查清单显示尚未公布的民意调查(即pub_date未来的民意调查 )。我们来解决这个问题。
在教程4中,我们介绍了一个基于类的视图,基于ListView:
民调/的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()方法并对其进行更改,以便通过比较来检查日期timezone.now()。首先我们需要添加一个导入:
民调/的views.py ¶
from django.utils import timezone
然后我们必须get_queryset像这样修改方法:
民调/的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())返回一个查询集,其中包含的Questions pub_date小于或等于 - 即早于或等于 - timezone.now。
测试我们的新视图¶
现在,您可以通过启动runserver,在浏览器中加载站点,Questions在过去和将来创建日期以及检查是否仅列出已发布的日期来满足您自己的行为 。您不希望每次进行任何可能影响此更改的更改时都这样做- 所以我们也要根据上面的shell会话创建一个测试。
将以下内容添加到polls/tests.py:
民调/ tests.py中¶
from django.urls import reverse
我们将创建一个快捷函数来创建问题以及一个新的测试类:
民调/ tests.py中¶
def create_question(question_text, days):
“”"
Create a question with the given question_text
and 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 QuestionIndexViewTests(TestCase):
def test_no_questions(self):
“”"
If no questions exist, an appropriate message is 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_past_question(self):
"""
Questions with a pub_date in the past are 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_future_question(self):
"""
Questions with a pub_date in the future aren't 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.")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_future_question_and_past_question(self):
"""
Even if both past and future questions exist, only past questions
are 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_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_no_questions不会产生任何问题,但会检查消息:“没有可用的民意调查。”并验证是否latest_question_list为空。请注意,django.test.TestCase该类提供了一些额外的断言方法。在这些例子中,我们使用 assertContains()和 assertQuerysetEqual()。
在test_past_question,我们创建一个问题并验证它是否出现在列表中。
在test_future_question,我们pub_date将来会创建一个问题。为每个测试方法重置数据库,因此第一个问题不再存在,因此索引不应该有任何问题。
等等。实际上,我们使用测试来讲述网站上管理员输入和用户体验的故事,并检查每个州的状态以及系统状态的每次新变化,都会发布预期结果。
测试DetailView¶
我们的工作做得很好; 但是,即使未来的问题没有出现在索引中,如果用户知道或猜到正确的URL,他们仍然可以联系到他们。所以我们需要添加一个类似的约束DetailView:
民调/的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())
当然,我们将增加一些测试,以检查一个Question,其 pub_date在过去可以显示,而一个具有pub_date 在未来是不是:
民调/ tests.py中¶
class QuestionDetailViewTests(TestCase):
def test_future_question(self):
“”"
The detail view of a question with a pub_date in the future
returns a 404 not found.
“”"
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):
"""
The detail view of a question with a pub_date in the past
displays the question's 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)
更多测试的想法¶
我们应该为该视图添加一个类似的get_queryset方法ResultsView并创建一个新的测试类。它与我们刚创造的非常相似; 事实上会有很多重复。
我们还可以通过其他方式改进我们的应用程序,并在此过程中添加测试。例如,Questions可以在没有的网站上发布它是愚蠢的Choices。因此,我们的观点可以检查这一点,并排除这种情况 Questions。我们的测试会创建一个Question没有Choices,然后测试它没有发布,以及创建一个类似Question 的 Choices,并测试它是否已发布。
也许应该允许登录的管理员用户看到未发布的Questions,但不能看到 普通的访问者。再说一次:无论需要添加到软件中来实现这一点,都应该伴随着测试,无论是先编写测试还是让代码通过测试,或者首先在代码中计算出逻辑,然后再编写测试证明给我看。
在某个时刻,你一定会看看你的测试,并想知道你的代码是否遭受了测试膨胀,这导致我们:
测试时,越多越好¶
我们的测试似乎越来越失控。按照这个速度,我们的测试中的代码很快会比我们的应用程序中的代码更多,并且与其他代码的优雅简洁相比,重复是不美观的。
没关系。让他们成长。在大多数情况下,您可以编写一次测试然后忘掉它。在您继续开发程序时,它将继续执行其有用的功能。
有时需要更新测试。假设我们修正了我们的观点,以便只Questions与Choices发布。在这种情况下,我们现有的许多测试都会失败 - 告诉我们究竟需要修改哪些测试以使它们更新,所以在这种程度上测试有助于照顾自己。
在最坏的情况下,当您继续开发时,您可能会发现您有一些现在多余的测试。即使这不是问题; 在测试中的冗余是一个很好的事情。
只要您的测试得到合理安排,它们就不会变得难以管理。良好的经验法则包括:
TestClass每个模型或视图单独一个
针对要测试的每组条件的单独测试方法
描述其功能的测试方法名称
进一步测试¶
本教程仅介绍一些测试基础知识。你可以做很多事情,并且可以使用一些非常有用的工具来实现一些非常聪明的事情。
例如,虽然我们的测试涵盖了模型的一些内部逻辑以及我们的视图发布信息的方式,但您可以使用“浏览器内”框架(如Selenium)来测试HTML在浏览器中实际呈现的方式。这些工具不仅可以检查Django代码的行为,还可以检查JavaScript的行为。很有可能看到测试启动浏览器,并开始与您的网站进行交互,就像一个人在驾驶它一样!Django包括LiveServerTestCase 促进与Selenium等工具的集成。
如果您有一个复杂的应用程序,您可能希望在每次提交时自动运行测试以实现持续集成,以便质量控制本身 - 至少部分 - 自动化。
发现应用程序未经测试的部分的一个好方法是检查代码覆盖率。这也有助于识别脆弱甚至死亡的代码。如果您无法测试一段代码,通常意味着代码应该被重构或删除。覆盖范围将有助于识别死代码。有关详细信息,请参阅 与coverage.py集成。
Django中的测试具有关于测试的全面信息。