《TDD - Python Web》学习笔记

Test-Driven-Development for Python Web 开源学习链接中有本书的最新版内容以及相关讨论,非常值得阅读学习。

TDD中的重要概念

自动化测试金字塔

单元测试->组件测试(->集成测试->系统测试->人工探索式测试)。单元测试和验收测试首先是文档,然后才是测试。它们当然可以验证系统是否达到了具体指标,但它们更为重要的目的是如实描述系统的设计、结构和行为。

  • 单元测试:测试大多数异常路径。可以说是程序员写给程序员的正式设计文档,描述低层结构以及代码行为;
  • 功能测试(/组件测试/验收测试):测试成功路径、极端情况、边界状态和可选路径。业务方和QA一起完成的正式需求文档,描述系统功能;
  • 集成测试:测试大型系统中各组件间的正常通信(各组件是否协调);
  • 系统测试:最终的集成测试,测试系统是否已正确组装完毕。

测试的不同类型

  • 隔离测试(或者说纯粹的单元测试)与整合测试(integrated tests)
    单元测试的主要作用是验证应用的逻辑是否正确。隔离测试是纯粹的单元测试,它只测试一部分代码,且只有这部分代码(比如一个函数)能够让测试失败。而如果这个函数依赖于其他系统且破坏这个系统会导致测试失败,就说明这是整合测试。

  • 集成测试(Integration tests)
    集成测试用于检查被你控制的代码是否和你无法控制的外部系统完好集成。集成测试往往也是整合测试。

  • 系统测试
    如果说集成测试检查的是与外部系统的集成情况,那么系统测试就是检查应用内部多个系统间的集成情况。例如:检查数据库、静态文件和服务器配置在一起是否能正常运行。

网站开发中的有趣知识点

  • 用户故事:从用户的角度描述应用该如何运行,用来组织功能测试。

  • 预期失败:意料之中的失败,驱动开发。

  • “不测试常量”规则:单元测试的规则之一是“不测试常量”——单元测试要测试的其实是逻辑、流程控制和配置。

  • 重构:重构是指在功能不变的前提下改进代码,其首要原则是不能没有测试(保证重构前后的表现一致)。记住,重构时,修改代码或者测试,但不能同时修改。

  • 模板:模板是Django中一个很强大的功能,它能把Python变量代入HTML文本;还能使用模板标签来使用模板句法:

1
2
{{ var }} - 引入Python变量
{% block replaceable_part %}{% endblock %} - 模板标签
  • 单元测试/编写代码循环有时也叫遇红/变绿/重构:1. 先编写一个会失败的单元测试(遇红);2. 编写尽可能简单的代码让测试通过(变绿),就算作弊也行;3. 重构,改进代码使其更合理,可以用三角法判断什么时候应该重构代码。

  • 三角法:添加一个测试,专门为现有的某些代码编写用例,以此推断出普适的实现方式(之前的实现方法可能作弊了)。

  • 事不过三,三则重构

  • 回归:新添加的代码破坏了原本可正常使用的功能

  • 记在便签上的待办事项清单:在便签上记录编写代码过程中遇到的问题,等手头的工作完成后再回过头来解决

  • YAGNI:You aint gonna need it.

  • Django对撇号(apostrophe)会自动转码(HTML-escaped)

  • Django可以把每个模型对象与特定页面(URL)相关联

  • 使用硬编码的URL是不科学的

  • mocking:可以在单元测试中使用模拟技术测试外部依赖(如API),它可以避免重复和测试其他人的代码,但在使用过程中要记得用patch修饰器避免副作用。但mock太多可能会导致代码异味(它依赖于实现方式),要避免使用太多。另外还要注意模拟的对象在if语句中的表现可能有违常规,mock对象是一个正值,而且可以掩盖错误,因为它有所有的属性和方法!

  • Python类的动态性质:它们在运行时创建,并可以在创建之后进一步修改。

  • Python修饰器:装饰器本质上的作用就是为已经存在的对象添加额外的功能。装饰器的顺序:

1
2
3
4
5
6
@a
@b
@c
def f ():
pass
# 等价于 f = a(b(c(f)))
  • Test Fixtures-测试固件,是指使用测试数据预先填充数据库的过程,例如储存User对象及其相关的Session(会话)对象。值得注意的是要在Django中避免使用JSON格式的固件,因为一旦修改了模型,这种固件的维护将会变成一个噩梦…(这时推荐使用Django ORM或者factory_boy之类的工具)
  • 持续集成(Continuous Integration/CI)服务器搭建过程:
  1. 获得Jenkins服务器:获取一个具有控制权的服务器 -> 安装最新的Jenkins -> 配置Jenkins的安全设置;
  2. 安装插件和设置项目:安装插件(部署项目前需要安装的依赖)并设置虚拟显示器、失败截图和转储HTML(还要安装PhantomJS运行QUnit JavaScript测试) -> 设置项目 -> 构建项目;
  3. Bonus:把CI和过渡服务器连接起来:使用CI服务器部署代码到过渡服务器并在过渡服务器中运行功能测试。

记录一个在练习部署的时候踩到的一个大坑!

在第21章的练习中,要求将“发送邮件登录”功能部署到服务器上,这个功能在本地的开发服务器上测试成功,设置如下:

1
2
3
4
5
6
7
 # setting.py
EMAIL_HOST = 'smtp.qq.com'
EMAIL_HOST_USER = 'my_qq_num@qq.com'
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD')
EMAIL_PORT = 25
EMAIL_USE_TLS = True

结果部署后,过渡服务器上的功能测试失败,邮件无法发送。服务器日志中未显示任何error,只是提示WORKER TIMEOUT。比较搞笑的是,最开始以为是环境变量的问题,以为gunicorn没有读到我的EMAIL_PASSWORD(因为gunicorn在systemd中设置的EnviromentFile只为ExecStart下的进程服务,在shell中echo不出来),这个导致浪费了很多时间。最后Connection timeout issue sending email in Django中的回答给了我提示:可能是发件服务器端口的问题:

于是尝试server$ telnet smtp.qq.com 25得不到连接结果,发现是我的服务器无法连接到QQ邮箱服务器25端口造成的send_mail超时失败。。改用465端口及SSL加密设置,测试成功通过:

1
2
3
4
# setting.py
[...]
EMAIL_PORT = 465
EMAIL_USE_SSL = True

拓展学习

  • Gunicorn
  • MVC(模型-试图-控制器)
  • REST(表现层状态转化)

学习收获

读完一本书,其中技巧性的东西如果在平常不使用就会被淡忘,最重要的是理解和吸收书中的思想。

当初我是带着几个问题来学习这本书的:

  1. 什么是测试驱动开发?
  2. 为什么要用测试驱动开发?
  3. 怎样在开发的过程运用TDD的思想?
  4. TDD在网站开发中的具体方法?

现在大概可以一一解答了:

  1. 在编写可以真正实现功能的代码前,首先编写测试。只有在测试和预期一样失败后才能继续进行下一步。
  2. 测试不仅仅可以保障代码的功能,给我们修改代码的勇气,它更是一种文档,可以帮助我们实现更好的设计。比如功能测试是应用的说明书,它会提供一个人类可读、容易理解的故事,帮助我们开发具有所需功能的应用(还能保证我们不会无意中破坏这些功能);而单元测试则描述了代码希望实现的效果(应用中的每行代码都应该至少有一个单元测试),告诉我们应该怎样编写代码—— 功能测试站在高层驱动开发,单元测试从底层驱动我们做些什么。而且TDD还使得我们永远不会忘记接下来应该做什么——重新运行测试就知道要做的事了。总而言之,重视测试的终极原因是测试让开发变得更有乐趣。
  3. 使用遇红->变绿->重构的流程进行开发:
  4. 使用“由外而内的TDD”完成页面。其实上面双循环的TDD流程就体现了由外而内的思想:从外部设计系统,再分层编写代码。具体到网站开发中,就是:从表现层(GUI,模板与URL)逐步向内层移动,通过视图层或控制器层,最终到达模型层。这种方法的理念是由实际需要使用的功能驱动代码的编写,而不是在低层猜测需求。