【有书共读15】Python测试驱动开发 读书笔记08

遵守“不测试常量”规则,使用模板解决这个问题

看一下 lists/tests.py 中的单元测试。现在,要查找特定的 HTML 字符串,但这不是测试HTML 的高效方法。一般来说,单元测试的规则之一是“不测试常量”。以文本形式测试HTML 很大程度上就是测试常量。
换句话说,如果有如下的代码:
wibble = 3
在测试中就不太有必要这么写:
from myprogram import wibble assert wibble == 3
单元测试要测试的其实是逻辑、流程控制和配置。编写断言检测 HTML 字符串中是否有指定的字符序列,不是单元测试应该做的。
而且,在 Python 代码中插入原始字符串真的不是处理 HTML 的正确方式。我们有更好的方法,那就是使用模板。如果把 HTML 放在一个扩展名为 .html 的文件中,
先不说其他好处,单就得到更好的句法高亮支持这一点而言也值了。Python 领域有很多模板框架,Django 有自己的模板系统,而且很好用。来使用这个模板系统吧。

使用模板重构


现在要做的是让视图函数返回完全一样的 HTML,但使用不同的处理方式。这个过程叫作重构,即在功能不变的前提下改进代码。
功能不变是最重要的。如果重构时添加了新功能,很可能会产生问题。重构本身也是一门学问,有专门的参考书——Martin Fowler 写的《重构》(http://refactoring.com/)。
重构的首要原则是不能没有测试。幸好我们在做测试驱动开发,测试已经有了。检查一下测试能否通过,测试能通过才能保证重构前后的表现一直:
$ python3 manage.py test
[...] OK
很好!先把 HTML 字符串提取出来写入单独的文件。新建用于保存模板的文件夹 lists/
templates,然后新建文件 lists/templates/home.html,再把 HTML 写入这个文件 2。
     lists/templates/home.html
<html>
<title>To-Do lists</title>
</html>
高亮显示的句法,漂亮多了!接下来修改视图函数:

from django.shortcuts import render
def home_page(request):
return render(request, 'home.html')
现在不自己构建 HttpResponse 对象了,转而使用 Django 中的 render 函数。这个函数的第一个参数是请求对象的,第二个参数是渲染的模板名。Django 会自动在所有的应用目录中搜索名为 templates 的文件夹,然后根据模板中的内容构建一个 HttpResponse 对象。
模板是 Django 中一个很强大的功能, 使用模板的主要优势之一是能把
Python 变量代入 HTML 文本。现在还没用到这个功能,不过后面的章节会用到。这就是为什么使用 render 和 render_to_string(稍后用到),而不用原生的 open 函数手动从硬盘中读取模板文件的缘故。
看一下模板是否起作用了:
$ python3 manage.py test
[...]
======================================================================
ERROR: test_home_page_returns_correct_html (lists.tests.HomePageTest)➊
---------------------------------------------------------------------
Traceback (most recent call last):
File "/workspace/superlists/lists/tests.py", line 17, in test_home_page_returns_correct_html
response = home_page(request)➋
File "/workspace/superlists/lists/views.py", line 5, in home_page return render(request, 'home.html')➌
File "/usr/local/lib/python3.3/dist-packages/django/shortcuts.py", line 48, in render
return HttpResponse(loader.render_to_string(*args, **kwargs),
File "/usr/local/lib/python3.3/dist-packages/django/template/loader.py", line 170, in render_to_string
t = get_template(template_name, dirs)
File "/usr/local/lib/python3.3/dist-packages/django/template/loader.py", line 144, in get_template
template, origin = find_template(template_name, dirs)
File "/usr/local/lib/python3.3/dist-packages/django/template/loader.py", line 136, in find_template
raise TemplateDoesNotExist(name) django.template.base.TemplateDoesNotExist: home.html➍
---------------------------------------------------------------------
Ran 2 tests in 0.004s
又遇到一次分析调用跟踪的机会。
➊ 然后确认是哪个测试失败:很显然是测试视图 HTML 的测试。
➋ 然后找到导致失败的是测试中的哪一行:调用 home_page 函数那行。
➌ 最后,在应用的代码中找到导致失败的部分:调用 render 函数那段。
➍ 先看错误是什么:测试无法找到模板。
那为什么 Django 找不到模板呢?模板在 lists/templates 文件夹中,它就该放在这个位置啊。
原因是还没有正式在 Django 中注册 lists 应用。执行 startapp 命令以及在项目文件夹中存放一个应用还不够,你要告诉 Django 确实要开发一个应用,并把这个应用添加到文件
settings.py 中。这么做才能保证万无一失。打开 settings.py,找到变量 INSTALLED_APPS,把
lists 加进去:
     superlists/settings.py
# Application definition
INSTALLED_APPS = (
'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'lists',
)
可以看出,默认已经有很多应用了。只需把 lists 加到列表的末尾。别忘了在行尾加上逗号,这么做虽然不是必须的,但如果忘了,Python 会把不在同一行的两个字符串连起来, 到时你就傻眼了。
现在可以再运行测试看看:
$ python3 manage.py test
[...]
self.assertTrue(response.content.endswith(b'</html>'))
AssertionError: False is not true
糟糕,还是无法通过。
你能否看到这个错误取决于你使用的文本编辑器是否会在文件的最后添加一个空行。如果没看到,你可以跳过下面几段,直接跳到测试通过那部分。
不过确实有进展。看起来测试找到模板了,但最后三个断言失败了。很显然输出的末尾出了问题。我使用 print repr(response.content) 调试这个问题,发现是因为转用模板后在响应的末尾引入了一个额外的空行(\n)。按下面的方式修改可以让测试通过:

self.assertTrue(response.content.strip().endswith(b'</html>'))

这么做有点像作弊,不过 HTML 文件末尾的空白并不重要。再运行测试看看:
$ python3 manage.py test
[...] OK

对代码的重构结束了,测试也证实了重构前后的表现一致。现在可以修改测试,不再测试常量,检查是否渲染了正确的模板。Django 中的另一个辅助函数 render_to_string 可以给些帮助:

from django.template.loader import render_to_string [...]

def test_home_page_returns_correct_html(self): request = HttpRequest()
response = home_page(request)
expected_html = render_to_string('home.html') self.assertEqual(response.content.decode(), expected_html)
使用 .decode() 把 response.content 中的字节转换成 Python 中的 Unicode 字符串,这样就可以对比字符串,而不用像之前那样对比字节。
这次重构想表达的重点是,不要测试常量,应该测试实现的方式。做得好!

Django 提供了一个测试客户端,其中有用于测试模板的工具。后面的章节会用到这个测试客户端。现在使用的是低层工具,目的是让你弄明白其中的工作方式。可以看出,并不神奇。







#Python##测试##笔记##读书笔记#
全部评论

相关推荐

10-11 17:30
湖南大学 C++
我已成为0offer的糕手:羡慕
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务