第一部分我要介绍测试驱动开发(Test-Driven Development,TDD)的基础知识。我们会从零开始开发一个真实的 Web 应用,而且每个阶段都要先写测试。
这一部分涵盖使用 Selenium 完成的功能测试以及单元测试,还会介绍二者之间的区别。我会介绍 TDD 流程,我称之为“单元测试 / 编写代码”循环。我们还要做些重构,说明怎么结合 TDD 使用。因为版本控制对重要的软件工程来说是基本需求,所以我们还会用到版本控制系统(Git)。我会介绍何时以及如何提交,如何把提交集成到 TDD和 Web 开发的流程中。
我们要使用 Django,它(或许)是 Python 领域之中最受欢迎的 Web框架。我会试着慢慢介绍 Django 的概念,一次一个,除此之外还会提供很多扩展阅读资料的链接。如果你完全是刚接触 Django,那么我极力推荐你花时间阅读这些资料。如果你感觉有点儿茫然,花几小时读一遍 Django 的官方教程,然后再回来阅读本书。
你还会结识测试山羊……
复制粘贴时要小心
如果你看的是电子版,那么在阅读的过程之中就会很自然地会想要复制粘贴书中的代码清单。如果不这么做的话效果会更好:动手输入能形成肌肉记忆,感觉也更真实。你偶尔会打错字,这是
无法避免的,调试错误也是一项需要学习的重要技能。
除此之外,你还会发现 PDF 格式相当诡异,复制粘贴时经常会有意想不到的事情发生……
第 1 章 使用功能测试协助安装Django
TDD 不是天生就会的技术,而是像武术一样的一种技能。就像在功夫电影中一样,你需要一个脾气不好、不可理喻的师傅来强制你学习。我们的师傅是测试山羊。
1.1 遵从测试山羊的教诲,没有测试什也别做
在 Python 测试社区中,测试山羊是 TDD 的非官方吉祥物。测试山羊对不同的人有不同的意义。对我来说,它是我脑海中的一个声音,告诉我要一直走在测试这条正确的道路上,就像卡通片中浮现在肩膀上的天使或魔鬼一样,只是没那么咄咄逼人。我希望借由这本书,让测
试山羊也扎根于你的脑海中。
虽然还不太确定要做什么,但我们已经决定要开发一个网站。Web 开发的第一步通常是安装和配置 Web 框架。下载这个,安装那个,配置那个,运行这个脚本……但是,使用 TDD 时要转换思维方式。做测试驱动开发时,你的心里要一直记着测试山羊,像山羊一样专注,
咩咩地叫着:“先测试,先测试!”
在 TDD 的过程中,第一步始终一样:编写测试。
首先要编写测试,然后运行,看是否和预期一样失败,只有失败了才
能继续下一步——编写应用程序。请模仿山羊的声调复述这个过程。
我就是这么做的。
山羊的另一个特点是一次只迈一步。因此,不管山壁多么陡峭,它们
都不会跌落。看看图 1-1 里的这只山羊!
图 1-1:山羊比你想象的要机敏(来源:Flickr 用户 Caitlin
Stewart)
我们会碎步向前。使用流行的 Python Web 框架 Django 开发这个应
用。
首先,要检查是否安装了 Django,并且能够正常运行。检查的方法
是,在本地电脑中能否启动 Django 的开发服务器,并在浏览器中查
看能否打开网页。使用浏览器自动化工具 Selenium 完成这个任务。
在你想保存项目代码的地方新建一个 Python 文件,命名为
functional_tests.py,并输入以下代码。如果你喜欢一边输入代码一
边像山羊那样轻声念叨,或许会有所帮助:
functional_tests.py
from selenium import webdriver
browser = webdriver.Firefox()
browser.get('http://localhost:8000')
assert 'Django' in browser.title
这是我们编写的第一个功能测试(Functional Test,FT)。后面我
会深入说明什么是功能测试,以及它和单元测试的区别。现在,只要
能理解这段代码做了什么就行。
启动一个 Selenium webdriver,打开一个真正的 Firefox 浏览
器窗口。
在这个浏览器中打开我们期望本地电脑伺服的网页。
检查(做一个测试断言)这个网页的标题中是否包含单词
“Django”。
我们尝试运行一下:
$ python functional_tests.py
File ".../selenium/webdriver/remote/webdriver.py", line 268, in
get
self.execute(Command.GET, {'url': url})
File ".../selenium/webdriver/remote/webdriver.py", line 256, in
execute
self.error_handler.check_response(response)
File ".../selenium/webdriver/remote/errorhandler.py", line 194,
in
check_response
raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.WebDriverException: Message: Reached
error page: abo
ut:neterror?e=connectionFailure&u=http%3A//localhost%3A8000/[...]
你应该会看到弹出了一个浏览器窗口,尝试打开 localhost:8000,然
后显示“无法连接”错误页面。这时回到终端,你会看到一个显眼的
错误消息,说 Selenium 遇到了一个错误页面。接着,你会看到
Firefox 窗口停留在桌面上,等待你关闭。这可能会让你生气,我们
稍后会修正这个问题。
如果看到关于导入 Selenium 的错误,或者让你查找
“geckodriver”错误,或许你应该往前翻,看一下“准备工作和
应具备的知识”。
现在,得到了一个失败测试。这意味着,我们可以开始开发应用了。
别了,罗马数字
很多介绍 TDD 的文章都喜欢以罗马数字为例,闹了笑话,甚至我
一开始也是这么写的。如果你好奇,可以查看我在 GitHub 的页
面,地址是 https://github.com/hjwp/。
以罗马数字为例有好也有坏。把问题简化,合理地限制在某一范
围内,让你能很好地解说 TDD。
但问题是不切实际。因此我才决定要从零开始开发一个真实的
Web 应用,以此为例介绍 TDD。这是一个简单的 Web 应用,我希
望你能把从中学到的知识运用到下一个真实的项目中。
1.2 让Django运行起来
你肯定已经读过“准备工作和应具备的知识”了,也安装了 Django。
使用 Django 的第一步是创建项目,我们的网站就放在这个项目中。
Django 为此提供了一个命令行工具:
$ django-admin.py startproject superlists
这个命令会创建一个名为 superlists 的文件夹,并在其中创建一些
文件和子文件夹:
├── functional_tests.py
├── geckodriver.log
└── superlists
├── manage.py
└── superlists
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
在 superlists 文件夹中还有一个名为 superlists 的文件夹。这有
点让人困惑,不过确实需要如此。回顾 Django 的历史,你会找到出
现这种结构的原因。现在,重要的是知道 superlists/superlists 文
件夹的作用是保存应用于整个项目的文件,例如 settings.py 的作用
是存储网站的全局配置信息。
你还会注意到 manage.py。这个文件是 Django 的瑞士军刀,作用之
一是运行开发服务器。我们来试一下。执行命令 cd superlists,
进入顶层文件夹 superlists(我们会经常在这个文件夹中工作),然
后执行:
$ python manage.py runserver
Performing system checks...
System check identified no issues (0 silenced).
You have 13 unapplied migration(s). Your project may not work
properly until
you apply the migrations for app(s): admin, auth, contenttypes,
sessions.
Run 'python manage.py migrate' to apply them.
Django version 1.11.3, using settings 'superlists.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
暂时先不管关于“未应用迁移”的消息,第 5 章将讨论迁
移。
这样,Django 的开发服务器便在设备中运行起来了。让这个命令一直
运行着,再打开一个命令行窗口(进入刚刚打开的文件夹),在其中
再次运行测试:
$ python functional_tests.py
$
因为打开了新的终端窗口,所以要先执行 workon
superlists 命令激活虚拟环境。
我们在命令行中没执行多少操作,但你应该注意两件事:第一,没有
丑陋的 AssertionError 了;第二,Selenium 弹出的 Firefox 窗
口中显示的页面不一样了。
这虽然看起来没什么大不了,但毕竟是我们第一个通过的测试啊!值
得庆祝。
如果感觉有点神奇,不太现实,为什么不手动查看开发服务器,打开
浏览器访问 http:// localhost:8000 呢?你会看到如图 1-2 所示的
页面。
图 1-2:Django 可用了
如果想退出开发服务器,可以回到第一个 shell 中,按 Ctrl-C 键。
1.3 创建Git仓库
结束这章之前,还要做一件事:把作品提交到版本控制系统
(Version Control System,VCS)。如果你是一名经验丰富的程序
员,就无须再听我宣讲版本控制了。如果你刚接触 VCS,请相信我,
它是必备工具。当项目在几周内无法完成,代码越来越多时,你需要
一个工具查看旧版代码、撤销改动、放心地试验新想法,或者只是做
个备份。测试驱动开发和版本控制关系紧密,所以我一定要告诉你如
何在开发流程中使用版本控制系统。
好的,来做第一次提交。如果现在提交已经晚了,我表示歉意。我们
使用 Git 作为 VCS,因为它是最棒的。
我们先把 functional_tests.py 移到 superlists 文件夹中。然后执
行 git init 命令,创建仓库:
$ ls
superlists functional_tests.py geckodriver.log
$ mv functional_tests.py superlists/
$ cd superlists
$ git init .
Initialised empty Git repository in /.../superlists/.git/
自此工作目录都是顶层 superlists 文件夹
从现在起,我们会把顶层文件夹 superlists 作为工作目录。
(简单起见,我在命令列表中都将使用 /.../superlists/ 表示
这个目录。但实际上,这个目录的真实地址可能是 /home/kindreader-
username/my-python-projects/superlists/。)
我提供的输入命令都假定在这个目录中执行。同样,如果我提到
一个文件的路径,也是相对于这个顶层目录而言。因此,
superlists/settings.py 是指次级文件夹 superlists 中的
settings.py。
理解了吗?如果有疑问,就查找 manage.py,你要和这个文件在
同一个目录中。
现在,看一下要提交的文件:
$ ls
db.sqlite3 manage.py superlists functional_tests.py
db.sqlite3 是数据库文件,无须纳入版本控制。前面见过的
geckodriver.log 是 Selenium 的日志文件,也无须跟踪变化。我们
要把这两个文件添加到一个特殊的文件 .gitignore 中,让 Git 忽略
它们:
$ echo "db.sqlite3" >> .gitignore
$ echo "geckodriver.log" >> .gitignore
接下来,我们可以添加当前文件夹(“.”)中的其他内容了:
$ git add .
$ git status
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: .gitignore
new file: functional_tests.py
new file: manage.py
new file: superlists/__init__.py
new file: superlists/__pycache__/__init__.cpython-36.pyc
new file: superlists/__pycache__/settings.cpython-36.pyc
new file: superlists/__pycache__/urls.cpython-36.pyc
new file: superlists/__pycache__/wsgi.cpython-36.pyc
new file: superlists/settings.py
new file: superlists/urls.py
new file: superlists/wsgi.py
糟糕,添加了很多 .pyc 文件,这些文件没必要提交。将其从 Git 中
删掉,并添加到 .gitignore 中:
$ git rm -r --cached superlists/__pycache__
rm 'superlists/__pycache__/__init__.cpython-36.pyc'
rm 'superlists/__pycache__/settings.cpython-36.pyc'
rm 'superlists/__pycache__/urls.cpython-36.pyc'
rm 'superlists/__pycache__/wsgi.cpython-36.pyc'
$ echo "__pycache__" >> .gitignore
$ echo "*.pyc" >> .gitignore
现在,来看一下进展到哪里了。(你会看到,我使用 git status
的次数太多了,所以经常会使用别名 git st。我不会告诉你怎么
做,你要自己探索 Git 别名的秘密!)
$ git status
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: .gitignore
new file: functional_tests.py
new file: manage.py
new file: superlists/__init__.py
new file: superlists/settings.py
new file: superlists/urls.py
new file: superlists/wsgi.py
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working
directory)
modified: .gitignore
看起来不错,可以做第一次提交了:
$ git add .gitignore
$ git commit
输入 git commit 后,会弹出一个编辑器窗口,让你输入提交消
息。我写的消息如图 1-3 所示。
是不是 vi 弹出后你不知道该做什么?或者,你是不是看到了一个消息,内容是关于账户
识别的,其中还显示了 git config --global user.username ?再次看一下“准备
工作和应具备的知识”,里面有一些简单说明。
1
1
图 1-3:首次 Git 提交
如果你迫切想完成整个 Git 操作,此时还要学习如何把代
码推送到云端的 VCS 托管服务中,例如 GitHub 或 BitBucket。
如果你在阅读本书的过程中使用不同的电脑,会发现这么做很有
用。具体的操作留给你去发掘,GitHub 和 BitBucket 的文档写
得都很好。要不,你可以等到第 9 章,到时我们会使用其中一个
服务做部署。
对 VCS 的介绍结束。祝贺你!你使用 Selenium 编写了一个功能测
试,安装了 Django,并且使用 TDD 方式,以测试山羊赞许的、先写
测试这种有保障的方式运行了 Django。在继续阅读第 2 章之前,先
表扬一下自己吧,这是你应得的奖励。