Django1.5:(二)创建第一个Django应用5

       前面4节创建了第一个Django应用——Poll app。这里要写一下自动测试。

5.1 简介

       什么是自动测试?就是简单测试代码运行情况的。在不同层次上测试运行情况,有些测试非常细微,例如某个特定模型的特定方法是不是返回一个预期的值;也会在整体上做检验,比如用户的一些输入是否产生期望的结果。这和我们在第一节中用shell来检查一个方法或者运行app其实是一样的。

       为什么要写测试,为什么要现在写?或许你觉得学一下Python/Django就足够了,学test没有必要,毕竟现在Poll应用运转良好,不用画蛇添足地写测试代码。诚然,如果poll应用是你想学的最后一个Django程序,你确实不必学,但是如果想要继续深入,这个时候学习最好不过了。

       测试节省时间。“测试发现其工作”当然很好,但是由于程序可能复杂,各部分作用到一起,部分的修改会引发其他模块的错误,test代码可以帮助我们代码有问题的地方。有时候写测试代码似乎打断了我们创造性的生产工作,但是测试会比手动寻找错误更加有效。

       测试不仅识别而且组织错误。测试并不是开发的负面,没有测试,app的真实行为可能是不透明的,即使是自己写的代码,有时候也对其真正在做的事情不很明了。测试可以帮助我们将内部的东西外在化,在发生错误的时候我们可以聚焦到相应代码——即使是我们没有意识到它有问题。

       测试让代码更有吸引力。也许我们写了一个很赞的代码段,但是许多其他程序员不愿意去看仅仅是因为没有测试——没有测试,没有信任!Django的创始人之一Jacob Kaplan-Moss说“没有写测试的代码在设计上就是失败的。”只有写了测试代码别人才会认真地审视你工作。

       团队需要测试。如果复杂的Django开发是依靠团队的话,那必须要学会测试,如果是职业的程序员也必须要会测试。

5.2 基本的测试策略

       有很多方式写测试。有些程序员员采用“测试驱动开发”,在coding前在写好了测试,这似乎是反直觉的,但想一想,我们都是问一个问题,然后去解决,这不过是在Python编程中遵循了而已。对于新手是在写了一些代码后决定是否要写测试,或许这些测试越早写越好,但是也永远不会太迟。似乎没有合适些测试的标准,如果写了几千行代码,这时候很难决定测试什么,但是此时的测试往往在代码改动时很有成效。

5.3 第一个测试

识别一个bug

       正好在poll应用中有一个需要修正的漏洞:Poll.was_published_recently()能够正确判断过去的发布时间,但是对于未来则是错误的。我们可以在Admin中看到这一点,我们把发布时间设在3013年,它能显示最近发布。或者也可以在django shell中看到

>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Poll
>>> # create a Poll instance with pub_date 30 days in the future
>>> future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # was it published recently?
>>> future_poll.was_published_recently()
True
未来不是最近,这显然是个bug。
写一个揭示bug的测试

       我们在shell中做的就是我们要在自动测试中做的,应用中有相关的测试模块,其会自动检查,我们就在polls/tests.py中添加以下代码吧:

import datetime

from django.utils import timezone
from django.test import TestCase

from polls.models import Poll
class PollMethodTests(TestCase):
    def test_was_published_recently_with_future_poll(self):
    """
    was_published_rencently() should return False for polls whose
    pub_date is in the future
    """
    future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
    self.assertEqual(future_poll.was_published_recently(), False)
我们这里创建了django.test.TestCase子类,测试pub_date为未来时间时的Poll实例,返回结果应该是False才通过测试。

运行测试

       我们用命令行python manage.py test polls运行测试。结果为:

Creating test database for alias ’default’...
F
======================================================================
FAIL: test_was_published_recently_with_future_poll (polls.tests.PollMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_poll
self.assertEqual(future_poll.was_published_recently(), False)
AssertionError: True != False
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Destroying test database for alias ’default’...

背后的故事是这样子的:Python manage.py test polls需找polls应用中的tests,发现了django.test.TestCase的子类,创建了一个特殊的数据库用于测试,需找测试方法,也就是一些以test开头的函数,用assertEqual()方法,发现程序运行的结构式True,而要求是False,于是test指明了出错的位置。

修正错误

     我们已经知道了Poll.was_published_recently()这样的问题,我们需要在models.py中修改,使其只会在过去返回True:

    def was_published_recently(self):
        return timezone.now() - datetime.timedelta(days=1) <= self.pub_date < now
继续运行测试,发现可以通过。如果以后因为代码改动,导致这样的错误会立即报告。

更深入的测试

       我们可以继续让was_published_recently()完美工作,因为我们可能会因为修正了一个而引入了另一个。加入两个测试:

    def test_was_published_recently_with_old_poll(self):
        """
        was_published_recently() should return True for polls whose
        pub_date is older than 1 day
        """
        old_poll = Poll(pub_date=timezone.now() - datetime.timedelta(days=30))
        self.assertEqual(old_poll.was_published_recently(), False)
    def test_was_published_recently_with_recent_poll(self):
        """
        was_pubished_recently() should return True for polls whose
        pub_date is with the last day
        """
        recent_poll = Poll(pub_date=timezone.now() - datetime.timedelta(hours=1))
        self.assertEqual(recent_poll.was_published_recently(), True)
我们为其写了三个测试,polls是一个简单的应用,但是如果他未来变得更复杂,后者其他代码与其发生联系,我们可以保证这部分代码会像预期一样工作。

5.4 测试视图

       Poll应用还有一个问题,它会发布所以的polls,但是我们希望pub_date在未来的poll只有在那个时候才会发布,才能被看见。

视图的一个测试

       我们第一个测试其实就是简单的测试驱动开发,其主要关注代码的内部行为,这里我们测试用户通过浏览器浏览时的表现。在我们修正前,先看一下我们将要使用到的工具。

Django测试客户端

      Django提供了一个测试Client来模拟用户与视图层的代码交互过程,我们可以在tests.py甚至shell中使用。我们先打开shell,在这里我们先做些准备工作,首先启用测试环境:

>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
setup_test_environment()安装能了让我们简称额外属性的模板例如response.context,否则我们探测不到。注意这样方法没有启用一个测试数据库,其直接在现有数据库上运行,其输出部分依赖于现有polls。接下来我们需要导入测试客户端类(后面django.test.TestCase类要用到的客户端是其自带的):

>>> from django.test.client import Client
>>> # create an instance of the client for our use
>>> client = Client()
这样我们就可以让测试客户端做点事了:

>>> # get a response from ’/’
>>> response = client.get(’/’)
>>> # we should expect a 404 from that address
>>> response.status_code
404
>>> # on the other hand we should expect to find something at ’/polls/’
>>> # we’ll use ’reverse()’ rather than a harcoded URL
>>> from django.core.urlresolvers import reverse
>>> response = client.get(reverse(’polls:index’))
>>> response.status_code
200
>>> response.content
’\n\n\n <p>No polls are available.</p>\n\n’
>>> # note - you might get unexpected results if your ‘‘TIME_ZONE‘‘
>>> # in ‘‘settings.py‘‘ is not correct. If you need to change it,
>>> # you will also need to restart your shell session
>>> from polls.models import Poll
>>> from django.utils import timezone
>>> # create a Poll and save it
>>> p = Poll(question="Who is your favorite Beatle?", pub_date=timezone.now())
>>> p.save()
>>> # check the response once again
>>> response = client.get(’/polls/’)
>>> response.content
’\n\n\n <ul>\n \n <li><a href="/polls/1/">Who is your favorite Beatle?</a></li>\n \n >>> response.context[’latest_poll_list’]
[<Poll: Who is your favorite Beatle?>]
升级视图

       polls列表会显示未来发布的内容,让我们修正它。在上一节中,我们创建了基于类的视图:

class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_poll_list'
    
    def get_queryset(self):
        return Poll.objects.order_by('-pub_date')[:5]
我们需要修改get_query方法,使它与timezone.now()比较,实现导入timezone,再使之像这样:

def get_queryset(self):
        return Poll.objects.filter(pub_date_lte=timezone.now()).order_by('-pub_date')[:5]
测试我们的新视图

        可以创建不同时间的poll,然后再浏览器中查看。我们不需要一有改动就要这样做,我们可以写个测试,包括工厂函数和新的测试类:

def create_poll(question, days):
    """
    Creates a poll with the given 'question' published the given number of
    'days' offset to now (negative for polls published in the past,
    positive for polls that have yet to be published).
    """
    return Poll.objects.create(question=question, pub_date=timezone.now() + datetime.timedelta(days=days))
    
class PollViewTests(TestCase):
    def test_index_view_with_no_polls(self):
        """
        If no polls exist, an appropriate message should be 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_poll_list'], [])
    def test_index_view_with_a_past_poll(self):
        """
        Polls with a pub_date in the past should be displayed on the index page.
        """
        create_poll(question="Past poll.", days=-30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_poll_list'],
            ['<Poll: Past poll.>']
        )
    def test_index_view_with_a_future_poll(self):
        """
        Polls with a pub_date in the future should not be displayed on the
        index page.
        """
        create_poll(question="Future poll.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertContains(response, "No polls are available.", status_code=200)
        self.assertQuerysetEqual(response.context['latest_poll_list'], [])
    def test_index_view_with_future_poll_and_past_poll(self):
        """
        Even if both past and future polls exist, only past polls should be
        displayed.
        """
        create_poll(question="Past poll.", days=-30)
        create_poll(question="Future poll.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
        response.context['latest_poll_list'],
            ['<Poll: Past poll.>']
        )
    def test_index_view_with_two_past_polls(self):
        """
        The polls index page may display multiple polls.
        """
        create_poll(question="Past poll 1.", days=-30)
        create_poll(question="Past poll 2.", days=-5)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_poll_list'],
            ['<Poll: Past poll 2.>', '<Poll: Past poll 1.>']
        )
        )
首先,工厂函数create_poll,以一定的间隔创建poll,test_index_view_with_no_polls不产生polls,不是检查信息“No polls are available.”以及验证latest_poll_list是否为空。注意django.test.TestCase提供额外的一些断言方法,这里用了asserContains()和assertQuerysetEqual()。在test_index_view_with_a_past_poll,我们创建了一个poll并验证是否在list中出现。在test_index_view_with_a_future_poll中我们创建一个未来时间的pub_date的poll。数据库在每个方法测试时重置。

测试DetailView

      即使未来的polls在列表中不现实,但是如果用户能猜出URL,那么也是个问题,我们需要在DetailView中设置相应的限制:

class DetailView(generic.DetailView):
    model = Poll
    template_name = 'polls/detail.html'
    def get_queryset(self):
        return Poll.objects.filter(pub_date_lte=timezone.now())
当然,我们也添加一些测试,看能不能显示:
class PollIndexDetailTests(TestCase):
    def test_detail_view_with_a_future_poll(self):
        """
        The detail view of a poll with a pub_date in the future should
        return a 404 not found.
        """
        future_poll = create_poll(question='Future poll.', days=5)
        response = self.client.get(reverse('polls:detail', args=(future_poll.id,)))
        self.assertEqual(response.status_code, 404)
    def test_detail_view_with_a_past_poll(self):
        """
        The detail view of a poll with a pub_date in the past should display
        the poll’s question.
        """
        past_poll = create_poll(question='Past Poll.', days=-5)
        response = self.client.get(reverse('polls:detail', args=(past_poll.id,)))
        self.assertContains(response, past_poll.question, status_code=200)    
更多测试
       可以添加一个类似的get_queryset到ResultsView视图,创建一个新的测试类。也可以继续走下去,譬如不能允许一个没有选项的poll发布,用view来识别并且排除它,只能管理员看到,普通用户不能看到……

5.5 越多测试越好

       似乎我们的测试超出了控制,这样的话我们的测试模块将很快挤满了代码,而且许多重复工作很无聊,但是有一点:只要我们写好了,它将一直工作。当然也有些时候它需要升级,例如我们修改了视图,只允许带Choice的Poll发布,这样可以就要将测试代码做些修改了。

5.6 深入测试

       我们的测试还比较简单,Django也提供了一些高级的工作,请参考文档相应部分。


       下一节是关于静态文件的内容,欢迎回来!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值