编写你的第一个 Django 应用,第 5 部分
本教程从 教程第 4 部分 结束的地方开始。我们已经建立了一个网络投票应用程序,现在我们将为它创建一些自动化测试。
自动化测试简介
自动化测试是什么?
测试代码,是用来检查你的代码能否正常运行的程序。
测试在不同的层次中都存在。有些测试只关注某个很小的细节(某个模型的某个方法的返回值是否满足预期?),而另一些测试可能检查对某个软件的一系列操作(某一用户输入序列是否造成了预期的结果?)。其实这和我们在 教程第 2 部分,里做的并没有什么不同,我们使用 shell
来测试某一方法的功能,或者运行某个应用并输入数据来检查它的行为。
真正不同的地方在于,自动化 测试是由某个系统帮你自动完成的。当你创建好了一系列测试,每次修改应用代码后,就可以自动检查出修改后的代码是否还像你曾经预期的那样正常工作。你不需要花费大量时间来进行手动测试。
为什么你需要写测试
但是,为什么需要测试呢?又为什么是现在呢?
你可能觉得学 Python/Django 对你来说已经很满足了,再学一些新东西的话看起来有点负担过重并且没什么必要。毕竟,我们的投票应用看起来已经完美工作了。写一些自动测试并不能让它工作的更好。如果写一个投票应用是你想用 Django 完成的唯一工作,那你确实没必要学写测试。但是如果你还想写更复杂的项目,现在就是学习测试写法的最好时机了。
测试将节约你的时间
在某种程度上,能够「判断出代码是否正常工作」的测试,就称得上是个令人满意的了。在更复杂的应用程序中,组件之间可能会有数十个复杂的交互。
对其中某一组件的改变,也有可能会造成意想不到的结果。判断「代码是否正常工作」意味着你需要用大量的数据来完整的测试全部代码的功能,以确保你的小修改没有对应用整体造成破坏——这太费时间了。
尤其是当你发现自动化测试能在几秒钟之内帮你完成这件事时,就更会觉得手动测试实在是太浪费时间了。当某人写出错误的代码时,自动化测试还能帮助你定位错误代码的位置。
有时候你会觉得,和富有创造性和生产力的业务代码比起来,编写枯燥的测试代码实在是太无聊了,特别是当你知道你的代码完全没有问题的时候。
然而,编写测试还是要比花费几个小时手动测试你的应用,或者为了找到某个小错误而胡乱翻看代码要有意义的多。
测试不仅能发现错误,而且能预防错误
「测试是开发的对立面」,这种思想是不对的。
如果没有测试,整个应用的行为意图会变得更加的不清晰。甚至当你在看自己写的代码时也是这样,有时候你需要仔细研读一段代码才能搞清楚它有什么用。
而测试的出现改变了这种情况。测试就好像是从内部仔细检查你的代码,当有些地方出错时,这些地方将会变得很显眼——就算你自己没有意识到那里写错了。
测试使你的代码更有吸引力
你也许遇到过这种情况:你编写了一个绝赞的软件,但是其他开发者看都不看它一眼,因为它缺少测试。没有测试的代码不值得信任。 Django 最初开发者之一的 Jacob Kaplan-Moss 说过:“项目规划时没有包含测试是不科学的。”
其他的开发者希望在正式使用你的代码前看到它通过了测试,这是你需要写测试的另一个重要原因。
测试有利于团队协作
前面的几点都是从单人开发的角度来说的。复杂的应用可能由团队维护。测试的存在保证了协作者不会不小心破坏了了你的代码(也保证你不会不小心弄坏他们的)。如果你想作为一个 Django 程序员谋生的话,你必须擅长编写测试!
基础测试策略
有好几种不同的方法可以写测试。
一些开发者遵循 “测试驱动” 的开发原则,他们在写代码之前先写测试。这种方法看起来有点反直觉,但事实上,这和大多数人日常的做法是相吻合的。我们会先描述一个问题,然后写代码来解决它。「测试驱动」的开发方法只是将问题的描述抽象为了 Python 的测试样例。
更普遍的情况是,一个刚接触自动化测试的新手更倾向于先写代码,然后再写测试。虽然提前写测试可能更好,但是晚点写起码也比没有强。
有时候很难决定从哪里开始下手写测试。如果你才写了几千行 Python 代码,选择从哪里开始写测试确实不怎么简单。如果是这种情况,那么在你下次修改代码(比如加新功能,或者修复 Bug)之前写个测试是比较合理且有效的。
所以,我们现在就开始写吧。
开始写我们的第一个测试
首先得有个 Bug
幸运的是,我们的 polls
应用现在就有一个小 bug 需要被修复:我们的要求是如果 Question 是在一天之内发布的, Question.was_published_recently()
方法将会返回 True
,然而现在这个方法在 Question
的 pub_date
字段比当前时间还晚时也会返回 True(这是个 Bug)。
用djadmin:[](https://docs.djangoproject.com/zh-hans/5.0/intro/tutorial05/#id1)shell
命令确认一下这个方法的日期bug
$ 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
因为将来发生的是肯定不是最近发生的,所以代码明显是错误的。
创建一个测试来暴露这个 bug
我们刚刚在 shell
里做的测试也就是自动化测试应该做的工作。所以我们来把它改写成自动化的吧。
按照惯例,Django 应用的测试应该写在应用的 tests.py
文件里。测试系统会自动的在所有文件里寻找并执行以 test
开头的测试函数。
将下面的代码写入 polls
应用里的 tests.py
文件内:
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
你将看到类似以下的内容:
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)
-----------------------