Django基础5-测试

自动化测试简介

什么是自动化测试

测试是简单的日常,用于检测你的代码的运行情况。
测试通常运行在不同的层面。有些测试会作用于一些细节(一个模型的方法的返回值是否与预期一致?),而也有一些用来检查软件的整体运行(一系列的用户输入是否得到了需要的结果?)。这与你在教程2中使用shell检查方法的行为、以及输入数据并检测其行为,没有什么不同。
自动化测试的不同之处在于,测试工作是系统为你做的。你一次性创建一个测试集合,然后当你对APP做出了修改,你可以检测你的代码是否任然正常工作,而不需要花费时间来手动测试上。

为什么需要创建测试

那么为什么创建测试,又为什么现在创建呢?
你可能会觉得你只需要学python/Django就已经够了,而且再去学其他的内容可能会压力大而且没有必要。毕竟,我们的polls应用已经运行的很开心了;经历学习自动化测试的麻烦看起来并不能使它得倒任何改进。如果创建polls应用是你Django编程的最后一点工作,那的确是这样,你不需要知道如何创建自动化测试了。但是,如果不是那样,那么现在就是学习自动化测试的最佳时间。

测试会节省你的时间

在某种程度上, ‘看上去工作正常的检测’ 是一个符合要求的测试. 在更复杂的应用中, 模块之间可能会存在大量的复杂的交互。
这些组件中的某一个发生改变,可能会造成无法预料的程序行为。检测以确定应用仍然“看上去工作正常”,可能意味着对于你代码中的某个功能,你可能需要运行几十种不同的数据来做检测,以确定你并没有破坏什么-这将浪费你很多时间。

而自动化测试却可以帮你在几秒内做完这些检测。如果应用发生错误,测试也可以帮助你定位到产生非预期行为的代码。
有时候,写测试似乎是一件枯燥的工作,特别是当你的应用可以正常运作的时候。
然而,相比花费数个小时手动测试你的应用 或 尝试定位新发现的问题,写测试的工作是一件很有成就感的事情。

测试不只是找出问题,它们还阻止问题

认为测试对开发来说是没用的是一个错误的想法。
没有测试,应用行为可能会相当的不明确。甚至你自己写的代码,有时候你也会陷入尝试确定“应用到底做了什么”之中。
测试改变了这一切;它从内部点亮了你的代码,而且当出错时,它们会聚焦在出错的部分-设置你都没有发现已经出错了。

测试让你的代码更吸引人

你可能创建了一个杰出的软件,但是你会发现很多其他开发者会轻易地拒绝看你的代码,因为它缺少测试;没有测试,它们不会信任它。
Jacob Kaplan-Moss, Django最早的一位开发者, 说 “没有测试的代码是失败的设计.”

其他开发者在认真看你的代码之前,希望看到你的软件中的测试,这是另一个你开始写测试的原因。

测试有助于团队协同工作

前面几点是从独立开发者的维护一个应用的角度来写的。复杂的应用需要一个或多个team来维护。测试可以保障同时不会不小心地破坏了你的代码(反过来,你也不会不小心破坏他们的代码)。如果你想以Django程序员为生,你必须要擅长写测试。

基本测试策略

写测试的方式、方法有很多。
有些程序员遵循一种“测试驱动开发”的规则;他们真的在写代码之前就写好了测试。这看上去违反常规,但是实际上,这跟大多数人做事的方式是类似的:他们描述一个问题,然后创建一些代码去解决它。Test-driven development simply formalizes the problem in a Python test case.
通常,新手测试会创建一些代码,而后才会认识到应该有一些测试。或许早点写一些测试会更好,但是现在开始也不晚。
有时候,确定测试应该从何处开始写,是有困难的。如果你写过几千行python代码,选择一些需要测试的内容可能不容易。这种情况下,从下次你做一些修改或添加一个新特性或修改一个bug开始,写下你的第一个测试是富有成效的。

所以呢,让我们立即开始吧!

写下你的第一个测试

我们定位一个BUG

非常幸运,我们的polls应用有一个小bug需要我们立即处理掉:Question.was_published_recently() 方法返回True,如果Question在昨天(正确)发布,但是如果Question的pub_date字段是在未来的一个日期(这确定是不可能的)。
确认这个bug,通过使用shell,来检测该方法,使用一个假设的未来的日期:

192:mysite fww$ python3 manage.py shell
Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 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

可以看到,我们创建一个日期在未来的问题,调用was_published_recently()方法,结果返回的是True。这显然是不合理的,因为未来的东西,又怎么能是最近的呢?

写一个测试来暴露这个BUG

我们在命令行中所做的测试,正是我们需要在自动化测试中去做的,因此让我们将这些东西转到自动化测试中去吧。

应用的特使有一个约定的位置,那就是应用中的tests.py文件;测试系统会在任意文件以‘test’开头的文件中自动寻找测试。
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()的输出-应该是False。

运行测试

在命令行输入如下命令:

python3 manage.py test polls
192:mysite fww$ python3 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 "/Users/fww/Documents/django/mysite/polls/tests.py", line 19, 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

测试通知我们哪个测试失败了,甚至错误出在哪一行代码。

解决这个BUG

我们已经知道了问题所在: Question.was_published_recently() 应该返回False,当pub_date属于未来时。 在 models.py中修改该方法, 使得它尽在日期属于过去时返回True:

def was_published_recently(self):
    now = timezone.now()
    return now - datetime.timedelta(days=1) <= self.pub_date <= now

再次运行测试:

192:mysite fww$ python3 manage.py test polls
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'...

在定位bug之后, 我们写了一个测试来暴露它,然后在代码中修正了这个bug,因此我们的测试通过了。
在未来,我们的应用可能还会遇到很多其他错误,但我们可以确定,不会再不小心地又制造出这个bug,因为简单滴运行一下测试就会立即警告我们。我们可以认为应用的这一小部分是永远安全的。

更全面的测试

走到这一步,我们可以进一步约束was_published_recently()方法;事实上,处理掉一个bug时,我们往往又回发现更多bug,这是令人沮丧的。
再添加两个测试方法到同一个类,更全面的测试该方法的行为:

# 如果问题是一天前发布的,那么was_published_recently()应该返回False
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)
# 如果问题是一天内发布的,那么was_published_recently()应该返回True
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是一个简单的应用,然而不管未来它变得多么复杂,也不管它将与什么代码做交互,我们现在可以有一定的保证,写了测试的方法的行为将是我们预期那样的。

测试视图

polls可以发布任何问题,包括pub_date字段在未来的问题。我们应当改进这一点。设置一个未来的发布时间意味着,该问题是那个时间发布的,而且在那一刻之前是不可见的。

为视图写测试(A test for a view)

当我们解决上述问题时,我们首先写测试,然后修改代码来解决BUG。事实上这就是测试驱动开发的一个简单例子,但是这个工作的步骤次序其实是无关紧要的。

在我们的第一个测试中,我们聚焦在代码的内部行为。接下来的这个测试,我们想检测一下,其在浏览器中的行为是否符合预期。

在开始之前,我们先看看有什么工具可以使用。

Django测试客户端(The Django test client)

Django提供了一个测试客户端,通过视图层面的代码,来模拟用户交互。我们可以在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的一些响应的属性,而其他的属性不可用。注意,该方法不会构建一个测试数据库,所以以下操作将会在已经存在的数据库上运行,而且输出可能会稍稍不同,这取决于你已经创建的问题。如果你的settings.py中的TIME_ZONE设置不正确,你可能会得到预期之外的结果。如果你之前忘记了设置它,在继续之前设置一下。
接下来,我们需要导入测试客户端类(后面在tests.py 中我们将使用django.test.TestCase class, 所以这一步不是必须的):

>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = Client()

这一切就绪之后, 我们就可以让客户端帮我们做些事情了:

fwwdeair:mysite fww$ python3 manage.py shell
Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
>>> from django.test import 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><a href="/polls/6/">\xe8\xbf\x99\xe6\x98\xaf\xe7\xac\xac\xe5\x85\xad\xe4\xb8\xaa\xe9\x97\xae\xe9\xa2\x98\xef\xbc\x9f</a></li>\n       \n            <li><a href="/polls/5/">\xe8\xbf\x99\xe6\x98\xaf\xe7\xac\xac\xe4\xba\x94\xe4\xb8\xaa\xe9\x97\xae\xe9\xa2\x98\xe5\x90\x97\xef\xbc\x9f</a></li>\n    \n            <li><a href="/polls/4/">\xe9\x97\xae\xe9\xa2\x98\xe5\x9b\x9b\xef\xbc\x9f</a></li>\n        \n            <li><a href="/polls/3/">\xe9\x97\xae\xe9\xa2\x98\xe4\xb8\x89\xef\xbc\x9f</a></li>\n        \n            <li><a href="/polls/2/">\xe9\x97\xae\xe9\xa2\x98\xe4\xba\x8c\xef\xbc\x9f</a></li>\n        \n    </ul>\n'
>>> response.context['latest_question_list']
<QuerySet [<Question: 这是第七个问题?>, <Question: 这是第六个问题?>, <Question: 这是第五个问题吗?>, <Question: 问题四?>, <Question: 问题三?>]>
>>>

其中第七个问题的pub_date是一个未来时间。

改进我们的视图

polls的list显示出了还没发布的问题(比如,pub_date是一个未来时间的问题)。让我们修改一下:
在教程4中,我们使用了一个派生子ListView的基于类的视图:

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()方法,让它将发布日期与当前日期做个比较,首先导入:

from django.utils import timezone

然后修改get_queryset方法如下:

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()).order_by('-pub_date')[:5]
这句代码有待进一步理解。其基本含义是过滤出“小于而不等于“当前时间的问题。

测试我们的新视图

在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):
        """
        pub_date为“过去”的问题,应当显示在主页上。
        注意:并不是你一定能在主页上看到它们,因为主页上只显示了最近的5个问题。
        这里的意思应该是过去的问题是可以显示的。比如说,过去你添加了问题,之后很久没添	
        加问题,那么过去的问题应该被显示。
        """
        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.>']
        )
fwwdeair:mysite fww$ python3 manage.py test polls
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
........
----------------------------------------------------------------------
Ran 8 tests in 0.044s

OK
Destroying test database for alias 'default'...

测试详情视图

尽管未来的问题不会显示在主页上, 但是用户依然可以使用正确的URL来访问到它们。 因此我们需要为详情视图添加类似的约束:

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())

添加详情视图的测试代码:

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)

更多测试的主意(Ideas for more tests)

我们应该给ResultsView也添加一个类似的get_queryset方法,并且创建新的测试类。这将非常的类似我们上面所做的事情;事实上,这其中有大量重复的工作。

我们还可以做更多测试。比如问题没有添加选项,用户不应该看见它,但是管理员可能需要能够看到。

测试越多越好

看上去我们写的测试代码,设置要比应用的代码还多了。不过这没关系,我们应该继续增加测试。因为测试一旦写好之后,你基本上就不需要再去理会它了,它将会持续在开发中发挥作用。

不过有时候,我们也需要更新测试。假设我们修改视图,使得只有包含选项的问题才能被发布。在这种情况下,我们的很多测试会失败。而失败的信息也会告诉我们哪些测试需要修改,以适用于最新的代码。

最坏的情况下,随着你持续开发,你可能会发现有些测试变成了多余的。 Even that’s not a problem;对于测试来说,冗余是一件好事.

如果你的测试明显是合理的,它们不会变得不可控制。好的经验包括以下几点:

  • 每个模型或视图应该有独立的测试类;
  • 每一组测试条件应该对应一个独立的测试方法;
  • 测试方法的名字应该包含其对应的函数名

进一步的测试

本教程只介绍了最基本的测试。你还有很多事情可做, 还有很多非常有用的工具可用来做一些聪明的事情。
更多内容参见“Django 测试”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值