实战Django:官方实例Part5

俗话说,人非圣贤,孰能无过。在堆代码的过程中,即便是老攻城狮,也会写下一些错误的内容。俗话又说,过而能改,善莫大焉。要改,首先要知道哪里存在错误,这便是我们要对投票应用进行测试的原因。

 

21.撰写第一个测试



在我们这个项目中,还真有一个bug存在。这个bug位于Question.was_published_recently() 方法中。当Question提交的日期是正确的,那没问题,但若提交的日期是错误的——比如日期是几天之后,问题就来了。

你可以在管理页面中增加一个投票,把日期设置在几天之后,你会发现你刚增加的投票被程序认为是“最近发布”的。

我们可以编写一段测试程序来界定问题。

编辑polls/tests.py 文件,添加下面的内容:

polls/tests.py

import datetime

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

from polls.models import Question

class QuestionMethodTests(TestCase):

    def test_was_published_recently_with_future_question(self):
        """
        如果question的发布日期是在将来,那么was_published_recently()应该
        返回一个False值。
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertEqual(future_question.was_published_recently(), False)

我们来运行一下测试,在Dos命令提示符下(注意,检查一下是否位于项目文件夹mysite下,,就象我们在Part1中所做的那样),输入:

python manage.py test polls

你会看到象这样的运行结果:

Creating test database for alias ‘default‘...
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
    self.assertEqual(future_question.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.py文档。
  • 然后它会发现我们的测试类:QuestionMethodTests
  • 程序会根据测试类创建一个临时数据库;
  • 在test_was_published_recently_with_future_question中,程序会创建一个Question实例,它的发布日期在30天之后;
  • 最后它使用了assertEqual() 方法,它发现was_published_recently() 返回的值是True,而实际上我们希望它返回的是False。

所以我们看到最终的结果是FAILED,说明我们的程序存在问题。

 

22.修复Bug



既然找到了问题所在,我们来修复它。

编辑polls/models.py 文件,作如下改动:

polls/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‘...
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

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

这回正常了。

23.更多综合性的测试



有时项目中不止一个bug,下面,我们再编写两个测试。

编辑polls/tests.py 文件,在QuestionMethodTests类下添加下面的内容:

polls/tests.py

def test_was_published_recently_with_old_question(self):
    """
    was_published_recently() should return False for questions whose
    pub_date is older than 1 day
    """
    time = timezone.now() - datetime.timedelta(days=30)
    old_question = Question(pub_date=time)
    self.assertEqual(old_question.was_published_recently(), False)

def test_was_published_recently_with_recent_question(self):
    """
    was_published_recently() should return True for questions whose
    pub_date is within the last day
    """
    time = timezone.now() - datetime.timedelta(hours=1)
    recent_question = Question(pub_date=time)
    self.assertEqual(recent_question.was_published_recently(), True)

现在,我们一共有三个测试来确认Question.was_published_recently() 在过去、最近、将来三个时间点上创建问题时返回正确的值。

当然。投票应用还只是一个非常简单的例子,相应的bug也不会很多。但在以后我们开发项目的过程中,我们会碰到一些复杂的应用,这时,测试就变得更加重要了。

24.测试视图



前面我们只是对应用的内部业务逻辑进行测试,接下来,我们要模拟用户操作,来测试我们的视图。

在Part4中,我们的Index视图使用了Django通用视图中的ListView,它的内容是这样的:

polls/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这个方法,让它在提取数据时检查发布时间,与当前时间进行比对。我们编辑polls/views.py,在文件头部先加入:

polls/views.py:

from django.utils import timezone

然后修正get_queryset方法 :

polls/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())可确保返回的结果集中的Question对象的发布日期早于或等于当前的时间。

现在我们来测试一下这个新的视图。

编辑polls/tests.py,先在文件头部加入:

polls/tests.py:

from django.core.urlresolvers import reverse

然后再加上以下内容:

polls/tests.py:

def create_question(question_text, days):
    """
    Creates a question with the given `question_text` 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 QuestionViewTests(TestCase):
    def test_index_view_with_no_questions(self):
        """
        If no questions 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_question_list‘], [])

    def test_index_view_with_a_past_question(self):
        """
        Questions with a pub_date in the past should be 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_index_view_with_a_future_question(self):
        """
        Questions with a pub_date in the future should not be 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.",
                            status_code=200)
        self.assertQuerysetEqual(response.context[‘latest_question_list‘], [])

    def test_index_view_with_future_question_and_past_question(self):
        """
        Even if both past and future questions exist, only past questions
        should be 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_index_view_with_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_index_view_with_no_questions不创建任何问题,只是检查当没有任何投票问题的时候,首页是否能返回“No polls are available.”这个信息,同时检查latest_question_list是不是空的。
  • 在test_index_view_with_a_past_question中,我们创建了一个问题,把它的发布时间设在了30天前,然后检查它是不是出现在首页的列表中;
  • 在test_index_view_with_a_future_question中,我们创建了一个问题,把它的发布时间设在了30天后,这里的每一个测试方法在执行的时候,数据库都会重置。所以我们在上一步测试中创建的那个问题不再存在,这样,首页的列表就应该是空的;

即使我们在首页视图中不再显示那些发布在将来时段的问题,但还会有用户通过合适的链接来访问到这些内容。这就意味着,我们要调整内容页的视图。

编辑polls/views.py,在DetailView中加入get_queryset方法:

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

我们同样要编写一些测试来检查这个视图是否起作用了。

编辑polls/tests.py,加入下列内容:

polls/tests.py:

class QuestionIndexDetailTests(TestCase):
    def test_detail_view_with_a_future_question(self):
        """
        The detail view of a question with a pub_date in the future should
        return a 404 not found.
        """
        future_question = create_question(question_text=‘Future question.‘,
                                          days=5)
        response = self.client.get(reverse(‘polls:detail‘,
                                   args=(future_question.id,)))
        self.assertEqual(response.status_code, 404)

    def test_detail_view_with_a_past_question(self):
        """
        The detail view of a question with a pub_date in the past should
        display the question‘s text.
        """
        past_question = create_question(question_text=‘Past Question.‘,
                                        days=-5)
        response = self.client.get(reverse(‘polls:detail‘,
                                   args=(past_question.id,)))
        self.assertContains(response, past_question.question_text,
                            status_code=200)

我们来简单分析一下这段测试:

  • 在test_detail_view_with_a_future_question中,我们创建了一个问题,把它的发布时间设置在5天后,然后模拟用户去访问,如果我们新的DetailView起作用的话,这个链接应该是空的,换句话说,访问这个链接时,用户会得到一个404的状态码。
  • 在test_detail_view_with_a_past_question中,我们创建了一个问题,把它的发布时间设置在5天前,同样模拟用户去访问,这种情况下,用户会得到的状态码应该是200,也就是说,链接是有效的。

我们还可以就更多的问题进行测试,同时根据测试优化我们的应用。

举个例子,用户在使用这个应用的过程中,发布投票时没有带任何的投票项,此时,我们的视图需要具备相应的检测功能,来防止这类事情的发生。我们可以编写一个测试,创建一个问题,让它不再任何投票项,通过这样的测试来界定问题,并根据测试结果对视图进行调整。

在测试中,我们奉行一个理念:测试越多越好。测试越多,说明我们的应用越可靠。或许有一天,我们的测试代码的数量甚至超过了正式代码,不必在意这些。测试只会让我们的代码愈来愈成熟。

 

【未完待续】

本文版权归舍得学苑所有,欢迎转载,转载请注明作者和出处。谢谢!

作者:舍得

首发:舍得学苑@博客园

时间: 2024-10-03 01:26:20

实战Django:官方实例Part5的相关文章

实战Django:官方实例Part1

[写在前面] 撰写这个实战系列的Django文章,是很久之前就有的想法,问题是手头实例太少,一旦开讲,恐有"无米下锅"之忧. 随着对Django学习的深入,渐渐有了些心得,把这些心得整理出来,以规范的.方便新人上手的撰写方式来写这个实战系列,相信对刚接触Django的童鞋会有一定的帮助. 舍得这里所用到的版本,为Django 1.7.1,Python 3.2,想要按照舍得讲解的实例上手练习的话,请务必与舍得所用的版本保持一致. 实例是在Windows系统下调试的,如果你采用的是非Win

实战Django:官方实例Part2

我们接着Part1部分往下讲.我们在part1中启动服务器后,并没有在管理页面中发现新添加的Polls应用,怎么办捏? 7.在管理界面中显示Question 只要注册一下这个应用就可以了.编辑polls/admin.py 文件,让它变成下面这个样子: polls/admin.py: from django.contrib import admin from polls.models import Question admin.site.register(Question) 这样Django就知道

实战Django:Rango Part5

在讲完注册模块之后,我们来着手实现登录和注销模块. 21.添加登录模块 注册功能做好了,接下来我们来完成登录模块. 以下是我们要完成的工作: 创建一个登录视图: 创建登录模板来显示登录表单: 将登录视图映射到URL: 在首页上添加登录链接: 我们先来搞定登录视图. 编辑rango/views.py,添加以下内容: rango/views.py: def user_login(request): # 如果是HTTP POST请求,则尝试取出相关的数据 if request.method == 'P

实战Django:Rango Part2

  在这一节,我们将学习如何用自动脚本来添加内容,然后给我们的应用添加首页和一个内容页. 9.Part1练习答案 除了加入views和likes两个字段,这里我们需要思考一个问题,用户将如何来访问我们的页面?用/rango/category/1/ 或 /rango/category/2/?他们怎么知道1代表哪个分类,2又代表哪个分类?有没有更直接一点的方法呢? 我想还是直接用分类名称来作为链接的一部分会比较好一些,比如用/rango/category/Python/来访问Python分类这样子.

实战Django:官方实例Part6

我们终于迎来了官方实例的最后一个Part.在这一节中,舍得要向大家介绍Django的静态文件管理. 现在,我们要往这个投票应用里面添加一个CSS样式表和一张图片. 一个完整的网页文件,除了html文档外,通常还需要其它一些文件,比如图片.JS或者CSS,这些文件,我们称之为"静态文件". 在那些小项目中,我们不需要对静态文件关注太多,因为你可以将这些文件随便放置,只要你的服务器能查找得到就好.但在大一点的项目中,尤其是那些拥有好多个应用的项目,静态文件的管理便变得非常重要,所以骚年,还

实战Django:简易博客Part1

舍得学习新技能的时候,通常不喜欢傻读书--捧着一本阐述该项技能的书籍,然后傻看,一路看下来,脑子里塞满了新的概念.知识点,头是越来越大,但技能却几乎没掌握半分. 多年来,舍得养成了用做实例来学习新技能的习惯.这可不是舍得的独创,在象棋.围棋界中,历来有"打谱"一说,就是根据棋谱,把一步步棋摆出来,在打谱的过程中,去感悟.去吸收,此时感悟到的.吸收进的,才是属于你自己的东西.新的技能就这样一步步发展起来,逐渐壮大. 就象Django官方文档那样,一个实例看起来很简单,但作为初学者,用&q

实战Django:简易博客Part2

在Part1中,我们完成了本项目的基本框架搭建,并完善了一下管理后台,现在我们来看如何设计面向公共的页面部分. 8.创建模板 首先,请在blog文件夹下建立templates文件夹,然后,在templates文件夹中建立一个叫bog的文件夹,在这个bog文件夹下新建一个archive.html文件,这个文件的路径应该是: bog/templates/bog/archive.html 把下面这些代码添加到archive.html中: bog/templates/bog/archive.html:

实战Django:Pastebin

  这是<Django Web开发指南>中的最后一个实例.如果说上一个实例Liveblog重点讲的是Django和Ajax的协作,那么我们在Pastebin中,将学习到Django和高亮语法JS的协作,顺便复习一下在Django中加入表单. 1.创建项目和应用 我们先来创建本实例的项目,在dos命令提示符下转到Scripts文件夹(如"c:\python32\Scripts"),然后运行如下命令: $ django-admin startproject pastebinpr

实战Django:Rango Part1

在前面我们已经陆续学习了六个Django的实例,其中一个来自Django官方的文档,另外五个来自<Django Web开发指南>.舍得介绍的每一个实例,都是按照官方文档的标准,力求让大家在根据文章去做自己的实例时,能够按"文"索骥,轻松再现整个实例的制作过程. 接下来,舍得要介绍一个大型的实例,这个项目叫做Rango,它来自Django圈内口碑甚佳的<How to Tango with Django>一书.事实上,此书就是围绕Rango这一个实例来展开的,而且,