第一个Django应用[part5]

第一个Django应用[part5]

介绍自动测试

一、什么是自动测试?

就是设置好一套测试条件,当对应用做了调整后,可以自动检查功能是不是还能满足初始设定,而不需要手动的进一逐一测试。

二、为什么需要创建测试?

1、测试会节省你的时间

测试可以保证应用能正常工作,在复杂的应用中经常时数十个复杂的组件进行交互。在这些组件中做一点点的修改都有可能产生未知的连锁反应。在使用数十种不同的测试数据进行测试,以保证应用的正常运行,这不正是节省了时间。如果出现了问题,测试还可以帮助识别导致意外行为的代码。

2、测试不仅仅可以指出问题,还可以防止问题发生

没有经过测试,应用的行为和目的让人迷惑,即使这是你自己写的程序,你也要花费一些时间去理解它要干什么。

3、测试使代码更有吸引力

你可能开发出了一个很棒的软件,但你会发现许多其他开发人员拒绝查看它,因为它缺乏测试;没有测试,他们不会相信它。其他开发人员在认真对待软件之前希望看到软件中的测试,这也是您开始编写测试的另一个原因。

4、测试可以让团队协作

前面的几点是从维护应用程序的单个开发人员的角度出发的。复杂的应用程序将由团队维护。测试可以保证同事不会无意中破坏你的代码(你也不会在不知情的情况下破坏他们的代码)。如果你想成为Django程序员,就必须擅长编写测试!

基本测试策略

有些程序员遵循“测试驱动开发”(test-driven development)的原则;他们实际上是在写代码之前写测试。这可能看起来违反直觉,但实际上这类似于大多数人经常做的事情:他们描述一个问题,然后创建一些代码来解决它。测试驱动开发用Python测试用例将问题形式化。

写第一个测试

$ python manage.py 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

建立一个测试暴露问题

通常,测试放在应用程序的tests.py文件中;测试系统会自动在任何文件名以test开头的文件中查找tests。写测试文件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子类,其包含了一个pub_date日期在未来的Question的实例。我们检查was_published_recently()的输出——它会输出False.

运行测试

在终端里输入:

$ python manage.py test polls

之后会看到类似的输出:

Found 1 test(s).                                                                                                                               
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
F
======================================================================
FAIL: test_was_published_recently_with_futuren_question (polls.tests.QuestionModelTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "E:\practices\python\Django_test\mysite\polls\tests.py", line 10, in test_was_published_recently_with_futuren_question
    self.assertIs(future_question.was_published_recently(),False)
AssertionError: True is not False

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)
Destroying test database for alias 'default'...

其间发生了什么:

  • manage.py test pollspolls应用中查找测试。
  • 它找到一个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(),当pub_date字段的日期在未来时应该返回False。修改models.py中的方法。所以:

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

在未来,我们的应用程序可能会出现许多其他问题,但我们可以肯定不会无意中再次引入这个错误,因为运行测试会立即发出警告。我们可以认为应用程序的这一小部分永远是安全的。

更全面的测试

实际上在修复一个BUG的同时很有可能会建立另一个BUG。添加更多的测试方法到同一个类中,为了更全面的测试这个方法:

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模型方法已经有三条测试方案了,随着应用功能的增加会逐渐加入新的测试方案。

测试视图

在我们的第一个测试中,我们密切关注代码的内部行为。对于这个测试,我们希望检查它的行为,就像用户通过web浏览器体验到它一样。在我们尝试解决任何问题之前,让我们看看我们可以使用的工具。

Django测试客户端

Django提供了一个仿真程序Client用于模拟用户在视图层的交互式访问。我们可以使用它在tests.py中,甚至在shell中进行测试。

再次从shell开始,在那里我们需要做一些在tests.py中不是必须做的事情——设置测试环境:

$ python manage.py shell
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()

setup_test_environment()安装一个模版渲染器,它允许我们检查响应上的一些额外属性,如:response.context,如果没有模版渲染器,就没办法做这些检查。注意这个方法并不设置测试数据库,所以接下来的操作依赖于现存的数据库,并且输出会依赖于Question库中已经建立的记录。如果settings.py中的TIME_ZONE没有设置正确,可能会有未知结果。

下面导入test client类(在之后的tests.py将使用django.test.TestCase类,它有自己的视图层客户端,所以这里并不需要):

>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = 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
404
>>> # on 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
200
>>> response.content
b'\n    <ul>\n    \n        <li><a href="/polls/1/">What&#x27;s up?</a></li>\n    \n    </ul>\n\n'
>>> response.context["latest_question_list"]
<QuerySet [<Question: What's up?>]>

改进我们的视图

polls的列表显示了尚未公布的名单,我们要对get_queryset()方法进行改进,让它也对比timezone.now(),首先在polls/views.py添加头文件:

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())返回一个查询集,它包含 了Question表中pub_date小于等于timezone.now()的记录,也就是早于或等于timezone.now()的时间。

测试我们的视图

现在可以打开浏览器查看我们修改的结果,但是如果不想每次都打开浏览器验证的话,那就需要我们的测试程序了,修改polls/tests.py

from django.urls import reverse

然后我们创建一个快捷函数来建立question记录,还有一个新的测试类:

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.
        """
        question = create_question(question_text="Past question.", days=-30)
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"],
            [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.
        """
        question = 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],
        )

    def test_two_past_questions(self):
        """
        The questions index page may display multiple questions.
        """
        question1 = create_question(question_text="Past question 1.", days=-30)
        question2 = create_question(question_text="Past question 2.", days=-5)
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"],
            [question2, question1],
        )

现在看一下上面的测试过程:

首先是question的快捷函数:create_question,用于在创建question时减少一些重复。

test_no_questions并不创建任何question,但是检查消息“No polls are available.”并验证latest_question_list是空。注意:django.test.TestCase类提供额外的断言方法。在上面的例子中使用了assertContains()assertQuerySetEqual().

test_past_question中,创建一个question并验证它是否出现在列表中。

test_future_question中,以未来时间创建一个question。每个测试方法都会重置数据库,因此第一个问题不再存在,因此索引中也不应该有任何问题。

实际上,我们用这些测试来讲述一个管理员输入和网站用户体验的故事,并检查在系统状态的每一个新变化时,是否发布了预期的结果。

测试DetailView

之前的工作没什么问题;然而,即使未来的question不出现在索引中,用户仍然可以访问它们,只要他们知道或猜对了URL。所以我们需要给DetailView添加一个类似的约束:

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

然后,我们应该添加一些测试,以检查可以显示pub_date为过去的Question,而不能显示pub_date为未来的Question:

poll/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)
Comments are closed.