第 4 章 编写这些测试有什么用
4.1 编程就像从井里打水
Kent Beck(TDD 里面基本上就是他发明的)。TDD 里面好比是一个棘轮,使用它你可以保存当前的进度,休息一会儿,而且能保证进度绝不倒退。这样你就没必要一直那么聪明了。
细化测试每个函数的好处
作者赞成为简单的函数编写细化的简单测试。
首先,既然测试那么简单,写起来就不会花很长时间。
其次,占位测试很重要。先为简单的函数写好测试,当函数变复杂后,这道心理障碍就容易迈过去。你可能会在函数中添加一个 if 语句,几周后再添加一个 for 循环,不知不觉间就将其变成一个基于元类(meta-class)的多态树状结构解析器了。因为从一开始你就编写了测试,每次修改都会自然而然地添加新测试,最终得到的是一个测试良好的函数。相反,如果你试图判断函数什么时候才复杂到需要编写测试的话,那就太主观了,而且情况会变得更糟糕。因为没有占位测试,此时开始编写测试需要投入很多精力。
不要试图找一些不靠谱的主观规则。
4.2 使用 Selenium 测试用户交互
重新运行测试找一下之前进展到哪里了 python functional_tests.py
TDD 的优点之一就是,永远不会忘记接下来该做什么——重新运行测试就知道要做的事了。
失败消息说“Finish the test”(结束这个测试),所以接着就是扩充其中的功能测试了:
我们使用了 Selenium 提供的几个用来查找网页内容的方法:find_element_by_tag_name
,find_element_by_id
和 find_elements_by_tag_name
(注意有个 s,也就是说这个方法会返回多个元素)。还使用了 send_keys,这是 Selenium 在输入框输入内容的方法。还会看到使用了 Keys 类,它的作用是发送回车键等特殊的按键,还有 Ctrl 等修改键。
小心 Selenium 中 find_element_by... 和 find_elements_by... 这两类函数的区别。前者返回一个元素,如果找不到就抛出异常;后者返回一个列表,这个列表可能为空。
看一下测试进展如何 python functional_tests.py
测试报错在页面找不到 <h1>
元素。
大幅修改功能测试后往往有必要提交一次,如下:
遵守“不测试常量”规则,使用模板解决这个问题
看一下 lists/tests.py 中的单元测试。现在,要查找特定的 HTML 字符串,但这不是测试 HTML 的高效方法。一般来说,单元测试的规则之一是“不测试常量”。以文本形式测试 HTML 很大程度上就是测试常量。
换句话说,如果有如下的代码:
在测试中就不太有必要这么写:
单元测试要测试的其实是逻辑、流程控制和配置。编写断言检测 HTML 字符串中是否有指定的字符序列,不是单元测试应该做的。
而且,在 Python 代码中插入原始字符串真的不是处理 HTML 的正确方式。我们有更好的方法,那就是使用模板。如果把 HTML 放在一个扩展名为 .html 的文件中,有很多好处,比如句法高亮支持等。Python 领域有很多模板框架,Django 有自己的模板系统,而且很好用。
使用模板重构
现在要做的是让视图函数返回完全一样的 HTML,但使用不同的处理方式。这个过程叫做重构,即在功能不变的前提下改进代码。
重构的首要原则是不能没有测试,我们正在做测试驱动开发,测试已经有了。测试能通过才能保证重构前后的表现一致: python manage.py test
测试通过后,先把 HTML 字符串提取出来写入单独的文件。新建用于保存模板的文件夹 lists/templates,然后新建文件 lists/templates/home.html,再把 HTML 写入这个文件。
接下来修改视图函数:
现在不自己构建 HttpResponse 对象了,转而使用 Django 中的 render 函数。这个函数的第一个参数是请求对象的,第二个参数是渲染的模板名。Django 会自动在所有的应用目录中搜索名为 templates 的文件夹,然后根据模板中的内容构建一个 HttpResponse 对象。
模板是 Django 中一个很强大的功能,使用模板的主要优势之一是能把 Python 变量代入 HTML 文本。这就是为什么使用 render 和 render_to_string,而不用原生的 open 函数手动从硬盘中读取模板文件的缘故。
看一下模板是否起作用了 python manage.py test
发现错误,测试无法找到模板,分析调用跟踪可知是调用 render 函数那段出错了。Django 找不到模板,是因为还没有正式在 Django 中注册 lists 应用。执行 startapp 命令以及在项目文件夹中存放一个应用还不够,你要告诉 Django 确实要开发一个应用,并把这个应用添加到文件 settings.py 中。这么做才能保证万无一失。打开 settings.py,找到变量 INSTALLED_APPS,把 lists 加进去:
可以看出,默认已经有很多应用了。只需把 lists 加到列表的末尾。现在可以再运行测试看看 python manage.py test
作者在
self.aseertTrue(response.content.endswith(b"</html>"))
出错了,原因是创建 HTML 文件时编辑器自动给末尾加了一个换行
自己的测试是通过的,所以对代码的重构结束了,测试也证实了重构前后的表现一致。现在可以修改测试,不再测试常量,检查是否渲染了正确的模板。Django 中的另一个辅助函数 render_to_string
可以给些帮助,在 lists/tests.py 文件中进行相应修改:
使用 .decode() 把 response.content 中的字节转换成 Python 中的 Unicode 字符串,这样就可以对比字符串,而不用像之前那样对比字节。
Django 提供了一个测试客户端,其中有用于测试模板的工具。
4.4 关于重构
重构时,修改代码或者测试,但不能同时修改。
重构后最好做一次提交:
4.5 接着修改首页
现在功能测试还是失败的。修改代码,让它通过。因为 HTML 现在保存在模板中,可以尽情修改,无需编写额外的单元测试。我们需要一个 <h1>
元素:
placeholder
为占位文字
得到了错误,找到表格,因此要在页面中加入表格。目前表格是空的:
功能测试的结果依旧是错误,准确地说是 assertTrue,因为没有给它提供明确的失败消息。可以把自定义的错误消息传给 unittest 中的大多数 assertX 方法:
再次运行功能测试,应该会看到我们编写的消息。
现在做个提交吧:
4.6 总结:TDD 流程
TDD 流程中涉及的主要概念:
功能测试
单元测试
“单元测试/编写代码”循环
重构
TDD 的总体流程,参照下图
首先编写一个测试,运行这个测试看着它失败。最后编写最少量的代码取得一些进展,再运行测试。如果不断重复,直到测试通过为止。最后,或许还要重构代码,测试能确保不破坏任何功能。
包含功能测试和单元测试的 TDD 流程,如下图所示
功能测试是应用是否能正常运行的最终判定。单元测试只是整个开发过程中的一个辅助工具。
这种看待事物的方式有时叫做“双循环测试驱动开发”。Emily Bache 写了一篇博客文章,从不同的视角讨论了这个话题,参考链接
使用 Git 检查进度
如果想进一步提升 Git 技能,可以添加作者的仓库,作为一个远程仓库:
git remote add harry https://github.com/hjwp/book-example.git
git fetch harry
然后可以按照下面的方式查看第 4 章结束时代码之间的差异:
git diff harry/chapter_04
Git 能处理多个远程仓库,因此就算已经把自己的代码推送到 GitHub 或者 Bitbucket,也可以这么做。
Last updated
Was this helpful?