Django教程之六-----编写你的第一个Django应用(5)

来源:互联网 发布:重庆seo外包服务价格 编辑:程序博客网 时间:2024/05/29 11:45

这个教程紧接(4).我们已经建立了一个问卷调查应用,并且我们现在创建一些自动化测试。

1. 引进自动化测试

1.1 什么是自动化测试?

测试时检测你代码运行的惯例。


测试在不同的级别上运行。一些测试可能应用一些小细节(做一个特殊模型的方法返回的值是否如期望?),其他的检查软件的整体操作(站点上用户输入的序列是否产生想要的结果?)。那和你在教程(2)中做的测试类型一样,使用shell来检查一个方法的行为,或者运行程序,输入数据来检查它怎么做。


自动测试的不同是测试工作是由系统代你来做。你创建一个测试序列,当你修改你的应用时,你能检查你的代码仍然如你原先设想般工作,而不需要花费时间手工测试。

1.2 为什么你需要创建测试

所以为什么创建测试,为什么现在?

你也许感觉仅仅学习Python/Django就十分足够了,还有另外一件事去学习和做也许看起来不可抗拒和非必需的。毕竟,我们的问卷调查应用现在运行的相当好;仔细检查创建自动测试的问题并不会使它运行的更好。如果创建问卷调查应用是你要做的最后一个Django编程,你不需要了解如何创建自动测试。但是,如果不是那样,现在将是一个学习的完美时间。

1.2.1 测试将会节省你的时间

在某一点上,‘检测它是否有用’将会是一个令人满意的工作。在一个更精细的应用中,你也许在组件之间有很多复杂的交互。


任何一个组件的改变可能会对应用的行为有一个不可预期的结果。检测它“看起来有用”意味着使用20个不同的测试数据的变量来功能性的运行你的代码来确保你没有弄坏什么东西 -- 这会浪费你的时间。


当自动测试能在几秒之内为你完成这些的时候,那尤为正确。如果有些东西出错了,测试将会帮助识别导致意外行为的代码。


有时候,把自己从富有成效的、创造性的编程工作中费力出来,去面对单调乏味的写作测试,尤其当你知道你的代码正常工作的时候,这似乎是一件苦差事。


然而,编写测试的任务比起手工花费数小时来测试你的应用或者尝试验证一个新引入问题的原因会更有意义。

1.2.2 测试不仅仅验证问题,他们防止问题

把测试看作开发的消极面是错误的。


没有测试,应用的目的或意图的行为将会十分难懂。即使是你自己的代码,有时候你会发现你在寻找答案的时候,试图寻找出来它到底在做什么。


测试改变了这些;它从内部点亮了你的代码,并且当有些事情跑偏时,它能聚焦到那些错误的地方 -- 甚至你还没意识到它有问题。

1.2.3 测试使你的代码更有吸引力

你也许创建过精彩的软件,但是你将会发现其他开发容易拒绝看它因为它缺少测试;没有测试,他们不值得相信。Jacob Kaplan-Moss,Django原始开发者之一,说过“没有测试的代码在设计上就是坏的”。


其他开发员想看你软件的测试,在他们认真之前,将是你开始编写测试的另一个理由。

1.2.4 测试帮组团队一起工作

前面的观点是从单个开发员维护应用来说的。复杂的应用将会有团队来维护。测试确保同事不会不经验的破坏你的代码(你也不会无意识的破坏他们的)。如果你想以Django编程员生活,你必须善于编写测试。


2.  基本测试策略

有许多种方法来编写测试。

一些程序员遵循一种被称为“测试驱动开发”的原则;他们在编写代码之前先编写测试。这也许看起来违反直觉,但实际上它和大多数人经常做的一样:他们描述问题,然后创建一些代码来解决它。测试驱动开发在Python测试例子中简单的形式化了这个问题。


进场的,测试新手将会创建一些代码并且稍后决定是否它应该要测试。也许早泄时候写测试会更好,但现在开始也不会太迟。


一些时候弄清楚在哪里开始编写测试时很困难的。如果你写过几千行Python代码,选择一些来测试不是那么容易。在这种情况下,在你下一次进行更改时编写你的第一个测试,或者在添加新的特性或者修复BUG时,都是很有成效的。

让我们立刻开始吧。

3. 编写我们的第一个测试

3.1  我们验证一个bug

幸运的,在polls应用程序中有一个小BUG需要我们立即修复:如果Question在最后一天出版(它是正确的)同样如果Question的pub_date字段是在将来(这肯定不是),Question.was_publicshed_recently()方法返回True。


为了检验是否bug真的存在,使用管理员创建一个问题它的时间是在将来,并且使用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

因为在未来的东西不是“最近”,这当然是错的。

3.2 创建测试来暴露BUG

我们刚刚在shell中做的对这个问题的测试正是我们能在自动化测试中做的,所以让我们使用自动化测试。

应用程序测试的惯例位置是在应用程序的tests.py文件中;测试系统会自动的寻找名称以test开头的任何文件中的测试。

将下列代码放到polls应用的tests.py文件里:

polls/tests.pyimport datetimefrom django.utils import timezonefrom django.test import TestCasefrom .models import Questionclass 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。

3.3 运行测试

在命令行中,我们能这样运行测试:

$ 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.001sFAILED (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
测试告诉我们那一个测试失败了,甚至是失败发生的行。

3.4 修复BUG

我们已经了解了问题所在:如果它的时间在未来的话,Question.was_published_recently()应该返回Fasle。在models.py中修改这个方法,所以它只会在时间是在过去时才会返回True:
polls/models.pydef 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.001sOKDestroying test database for alias 'default'...
当验证一个BUG后,我们编写一个测试将他暴露出来,并且在代码中修改bug所以我们的测试通过了。

未来我们应用的很多东西也许会出错,但是我们能确幸我们不会在无意中重新引入这个错误,因为简单的运行测试将会立刻警告我们。我们可以认为这个应用程序的一小部分被永远安全的固定下来。

3.5 更全面的测试

当我们在这时,我们可以进一步确认was_published_recently()方法;事实上,如果修复了一个BUG又引入了另外一个将会非常尴尬。


在相同的类中添加另2个测试方法,来更全面的测试我们方法的行为:

polls/tests.pydef 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个测试来确认Question.was_published_recently()返回合理的值在过去,刚才和未来的问题中。

再一次,polls是一个简单的应用,但是无论它在将来会变得多么复杂,无论它与什么其他代码交互,我们现在都可以保证,我们编写的测试方法将以预期的方式运行。

3.6 测试一个视图

问卷调查应用是相当不区分的:它将会发布任何问题,包括pub_date是在未来的问题。我们应该改进这一点。将一个pub_date设置为将来就意味着这个Question将会在那个时间发布,一直不可见直到那个时候。

3.6.1 关于视图的测试

当我们修复了上述的bug时,我们先编写测试,然后编写代码修复它。事实上,那就是测试驱动开发的例子,但我们以哪种顺序工作并不重要。

在我们第一个测试中,我们聚焦代码的内部行为。在这个测试中,我们想检查它的行为,因为它将有一个用户通过web浏览器来体验。

在我们尝试修改东西之前,让我们看看我们可以使用的工具。

3.6.2 Django 测试客户端

Django提供了一个测试Client来模拟一个用户在视图层与代码交互的情况。我们可以在tests.py或者甚至在shell中使用它。

我们在此启动shell,在这里我们要做一些在test.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类,它有自己的客户端,所以这不是必须的):

>>> 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_code404>>> # 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_code200>>> response.contentb'\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?>]>

3.6.3 改良我们的视图

问卷调查列表显示还没有发布的问卷(例如,那些pub_date在未来的)。让我们修正那个错误。

在教程(4)中,我们介绍了基于类的视图,基于ListView:

polls/views.pyclass 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()比较来检查日期。首先我们需要添加一个导入:

polls/views.pyfrom django.utils import timezone

然后我们需要像这样修改get_queryset方法:

polls/views.pydef 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的查询集,其中Questions的pub_date小于等于 -- 也就是说,早于或者等于 -- timezone.now
3.6.4 测试我们的新视图

现在可以启动服务来满足我们自己期望的行为,在你的浏览器中加载站点,用过去和未来的时间来创建Questions,然后检查只有那些已经发布的才会被列出来。你不必做那些在每次你做任何可能影响到这个的改变的时候 -- 所以让我们来创建一个测试,基于我们上述shell绘画的。

在polls/tests.py中添加如下:

polls/tests.pyfrom django.urls import reverse

我们将创建一个函数缩写来questions以及一个新的测试类:

polls/tests.pydef 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.>']        )

然我们更仔细的来查看这些。

首先是一个question缩写函数,create_question,来从创建questions中得到一些副本。


test_no_question不会创建任何questions,但是检测消息:“No polls are available”。并且验证latest_question_list是空的。注意,django.test.TestCase类提供了一些额外的断言方法。在这些例子中,我们使用assertContains()和assertQuerysetEqual()。


在test_future_question中,我们创建了一个带有未来pub_date的question。数据库会为每个测试方法重置,所以第一个问题已经不再哪里了,同样的index也不会有任何question。


以此类推。实际上,我们正在使用测试来讲述一个管理员输入和用户在站点体验的故事,并且在每一个状态和每一个系统状态上新的改变上检查,期望的结果被发布。

3.6.5 测试DetailView

我们工作做的很好;然而,即使是未来的question没有出现在index中,如果用户知道或者猜到正确的URL,他们仍然能够访问。所以我们需要添加一个相似的约束到detailView:

polls/views.pyclass 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在未来的话不会显示:

polls/tests.pyclass 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)

3.6.5 更多测试的想法

我们应该给ResultView添加一个相似的get_queryset方法并为那个视图创建一个新的测试类。它将和我们刚刚创建的很相似;事实上将会有很多的重复。


我们也能以其他形式改良我们的应用,并添加测试。例如,站点上的没有Choices的Questions能够发布就很蠢。所以,我们的视图能见检测这些,并排除这些quesitons。我们的测试将会创建一个没有Choices的Question并且测试它没有发布,同样创建一个有Choice的Question,测试它发布了。


也许登录的管理员用户应该允许看到未发布的Questions,但普通的登录者不行。再次:不管添加到软件的的任何用于完成这个的都应该进行测试,是否你首先写测试然后用代码通过测试,或者首先编写代码的逻辑,然后再写测试证明都可以。


在某一个时刻,你一定要查看你的测试,并且考虑是否你的代码正在遭受测试膨胀,它将会带给我们:


当测试时,越多越好

看起来好像我们的测试好像增长的超出了控制。按这个速率很快测试中的代码将会超过应用,与我们其他代码相比,这些重复是不具有美感的。


没关系。让他们增长吧。在大多数情况下,你可以编写一个测试一次然后忘记它。他将会继续执行它的帮助性函数就如你继续开发你的程序一样。


一些时候测试需要更新。假设我们修改我们的视图,所以只有带Choice的Question被发布,在哪种情况下,许多我们存在的测试都会失败 


只要你的测试是合理安排的,他们不会变得不可惯例。好的经验法则有:

  • 对每个模型或视图都有一个分开的TestClass
  • 对每个你想要测试的条件集都有单独的测试方法
  • 测试方法名称描述他们的函数

更进一步关于测试
这个教程只介绍了测试的一些基础。你可以做的更多,并且还有非常多有用的工具来完成一些非常漂亮的事情。

例如,当我们的测试涵盖了一些模型的内部逻辑和我们视图发布信息的方式,你可以使用一个“in-browser”框架例如Selenium来测试你的HTML实际上在浏览器中渲染的方式。这些工具允许你来检查你的Django代码的行为,同样,例如,你的JavaScript。看到测试启动一个浏览器,并且开始与你的站点交互,就好象人在操纵一样是很有意思的!Django包括LiveServerTestCase来促进像Selenium的集成。

如果你有一个复杂的应用,你也许想要在每次提交的时候运行自动测试,以实现持续集成,所以质量控制本身 -- 至少部分 -- 是自动的。

一个好的方法来认出你应用未测试的部分就是检查代码复用率。这同样帮助验证脆弱或者甚至死代码。如果你不能测试一个代码片,通常意味着那个代码应该重构或者移除。覆盖率会帮助验证死代码。阅读<Integration with coverage.py>来查看更多细节

<Django中的测试>有更综合性的关于测试的信息。

阅读全文
0 0
原创粉丝点击