0%

TDD 学习-知识汇总

TDD 是测试驱动开发的英文简称,是敏捷开发中的一项核心实践和技术,也是一种设计方法论。

TDD 理论知识

基础知识

TDD 是一种设计和开发方法,帮助我们从项目开始就构建出可运行的软件,并且以增量的方式添加新功能。
TDD 原则:编写代码只是为了修复失败的测试

Write new code only if an automated test has failed

TDD 周期: 测试 -> 编码 -> 重构 (测试先行),主要概念:

  • 单元测试(Given-When-Then)
  • “单元测试/编写代码”循环
  • 重构

TDD 特点:

  • 增量式开发
    以增量方式构建整个系统,距离已集成的、可工作的状态不会太远,也可降低风险。
  • 增量式演化设计
    在系统不断添加更多的功能和行为的过程中,不断地微调代码结构。在代码生命周期的任何时刻,代码所展现的都是为完成现有功能所做的最好的设计。 用这种方法,可以演化出能经受实践检验的架构。
  • 演进式设计
    通过小步的前进,在开发过程中逐渐地演化出最适合当前需求的架构。同时可以提高设计质量,从而提高整个系统的质量。

TDD 优点:

  • 永远不会忘记接下来做什么–重新运行测试就知道要做的事情了

TDD 难点:

  • 分解问题
    • 找到子问题之间的关联(通过输入、输出关联起来)
    • 找到问题的边界,明确假设与结果

TDD 流程:

  • 创建一个清单,列出所知道的需要让其运行通过的测试
  • 设计对象关系图
  • 通过一小段代码说明我们希望看到怎样的一种操作
  • 暂时忽略单元测试的一些细节问题
  • 通过建立存根(stub)来让测试程序通过编译
  • 通过一些另类的做法来让测试运行通过
  • 将新工作逐步加入计划清单,而不是一次全部提出
    描述 TDD 流程的另一种方式:
    • 遇红/变绿/重构
      先编写一个测试让它失败(遇红),然后编写代码让测试通过(变绿),最后重构(改进实现方式)。

回归

返回到更初级的状态。在软件开发领域,回归表明已有的、曾经可用的功能不再能正常运转,即从正常工作状态退化到不能工作的状态。 回归不会平白无故的发生,都是代码修改后引入了缺陷才会出现回归。因此必须依靠测试套件。

  • 测试套件
    • 在代码周围形成了一个模子。如果代码的改动破坏了功能,模子就不再符合了。若出现破损,也可以通过测试知道问题出现在何处。
    • 特点:
      1. 容易运行
      2. 能快速执行(快速获得反馈)

测试

  • 把测试作为沟通的共同语言
  • 用自动化测试做保护
  • 回归测试(Regression Test)
    在代码的修改后,随时可以执行所有测试(测试套件),测试失败了则可快速定位问题。
  • 好的测试是独立的
  • 好的测试时原子化的
  • 单元测试由功能测试驱动,而且更接近真正的代码
  • 以测试用例为规约

测试的最佳实践

  • 确保测试隔离,管理全局状态
  • 避免使用“含糊的”休眠

有时没法快速的完成所有测试。在这种情况下,可以选择运行一部分测试(通常是最可能发现当前改动所引发缺陷的那部分测试); 让构建服务器(build server,也称 continuous integration 持续集成服务器)在后台运行所有测试(单元测试、集成测试、功能测试)。 拥有持续集成服务器并不代表拥持续集成。持续集成服务器可以是构建过程自动化,还能产生精美的报表。

  • 持续集成服务: Jenkins

  • 持续集成:是指开发人员频繁的继承修改过的代码,使得集成几乎是持续的。

  • Martin Fowler-持续集成

  • 代码覆盖率
    通过工具来检测源代码中的问题或者分析源代码的复杂性
    需要合理的代码覆盖率

相关推荐:

在编写测试前,需要先弄清楚测试的目标,即实现的功能。然后将功能分解为需求

  • 传统的需求分解成任务;TDD 模式将需求分解为测试,创建测试列表
把模板子系统分解成任务(传统模式) 把模板子系统分解成测试(TDD模式)
写一个正则表达式以确定模板中的变量 没有任何变量的模板,渲染前后内容不变
实现一个使用正则表达式的模板解析器 实现一个模板引擎,引擎对外暴露一组API,内部实现使用模板解析器
含有一个变量的模板渲染后,变量应当替换为相应的值 含有多个变量的模板渲染后,变量应当替换成相应值
... ...

当在编写一个测试时,发现功能实现的不正确,或者整块缺失等问题,可以先把问题记录下来,继续手中的工作。这样可以集中注意力完成一项任务,而不是在几项任 务间来回切换。

功能测试

功能测试(functional test) = 验收测试(acceptance test) = 端到端测试(endto-end test)
ATDD(Acceptance Test Driven Development) 的总体流程:

  • 编写一个测试程序。编写一个故事,包含任何所有能想象到的、计算出正确结果所必需的的元素。
  • 让测试程序运行,并快速实测实通过。
    • 伪实现(bogus implementation) – 返回一个常量并逐渐用变量代替常量,至值伪实现代码称为真实实现的代码
    • 显明实现(obvious implementation) – 将真实的实现代码键入
    • 三角法(triangulation) – 根据两个例子(暂时不清楚实际语义,待查阅英文版本)
      • 适用于完全不知道该如何重构
  • 变现合格的代码(消除重复设计,优化设计结构)

编码

完成系统的途径: 深度优先广度优先

算法遍历树: 广度优先遍历,深度优先遍历

  • 广度优先:集中实现高层的功能,实现过程中会暂时使用底层功能的伪实现。
    • 先针对 Template 类的公共接口写测试,然后使用内部和底层功能的伪实现让测试通过,接着再去给下一层的功能写测试。
  • 深度优先:集中实现底层的功能,在所有底层功能都实现完成后才会组合起来实现出高层功能。纵向的实现功能的所有细节,一步步向前推进。
    • 先针对模板解析逻辑写测试。模板解析功能完成后再开始其他测试。

意图编程: 将注意力集中在能有的,而不是已经有的东西上。

重构

定义:

  • 名词形式: 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
  • 动词形式: 使用一系列重构手法,再不改变软件可观察行为的前提下,调整其结构。
  • 主要目的:消除重复
  • 方法:
    • 消除重复法

    • 三角法: 从多个角度同时入手,共同确定出恰当的实现方法。可以防止过早优化、功能蔓延(软件过分强调新的功能,以至于损害了其他的设计目标, 例如简洁性、轻巧性、稳定性及错误出现率等)以及总体上的过度设计。

    • 重命名法

      Refactoring is making changes to a body of code in order to improve internal structure, without changing its external behavior.


TDD with Django

  • 用户故事
    从用户的角度描述应用应该如何运行。用来组织功能测试
  • 预期失败
    意料之中的失败
  • 意外失败
    意料之外的失败。意味着测试中有错误,或者测试帮我们发现了一个回归,因此要在代码中修正
  • 测试代码尽量简单
  • 功能测试
    作用是帮助开发具有所需功能的应用,还能保证不会无意破坏这些功能。单元测试的作用是帮助编写简洁无错的代码。
  • 功能测试的调试技术
    • 改进错误消息
    • 手动访问网站
    • 等待时间(time.sleep)
  • 把工作分解成易于实现的小任务
  • 必要时做少量的设计
  • 不要预先做大量设计
  • 'YANGI’-(读作yag-knee) You ain’t gonna need id <你不需要这个>,不要编写当时看起来可能有用的代码
  • REST(式)-representational state transfer
  • 事不过三,三则重构
    • 判断何时删除重复代码时使用的经验法则。如果两个段代码很相似,往往还要等到第三段相似代码出现,才能确定重构时哪一部分是真正共同、可重用的。
  • 记在便签上的待办事项清单
    • 在便签上记录编写代码过程中遇到的问题,等手头的工作完成后再回过头来解决。
  • 更好的单元测试实践方法: 一个测试只测试一件事,但有时断言之间联系紧密,可以放在一起。
  • 确保测试隔离,管理全局状态
    • 不同的测试之间不能彼此影响,也就是说每次测试结束后都要还原永久状态。
  • 避免使用“含糊的”休眠
    • 一旦需要等待什么加载,第一反应是使用 time.sleep。但是这样做带来的问题是,时间的长度无法确定:要么太短,容易导致假失败;要么太长,会拖慢测试。 推荐使用重试循环,可以轮询应用,尽早向前行进。

功能测试新工具:通用显式等待辅助方法

  • 将测试放在单独的文件夹中
  • 对功能测试来说,按照特定功能或用户故事的方式组织
  • 对单元测试来说,使用一个名为 ‘tests’的文件夹,并创建 __init__.py 文件使其成为 python 包
  • 针对一个源码文件的测试放在一个单独的文件中。在Django中往往有test_models.py、test_views.py、test_forms.py
  • 每个函数和类都至少有一个占位测试
  • 编写测试的主要目的是重构代码。尽量让代码(包括测试)变得简洁。
  • 单元测试失败时不能重构

一般情況下: * 如果测试的对象还没实现,可以先为测试方法加上 @skip 装饰器 * 更常见的做法是:记下想重构的地方,完成已有的,等应用处于正常状态时重构 * 提交代码之前需要删除所有@skip装饰器

  • 尝试通用的 wait_for 辅助方法
    • 使用专门的辅助方法实现显式等待。能让测试更易于阅读,但有时单行断言或Selenium交互也需要等待一段时间。
    • 将辅助函数放在使用的功能测试类中,仅当辅助函数需要在别处使用时,才放在基类中,以防止基类太臃肿。 (这就是YANGI原则)

单元测试要测试的是:逻辑、流程控制和配置。不要测试常量,应该测试实现方式 编写断言检测 HTML 字符串是否有指定的字符,不是单元测试应该做的。

Django相关

  • MVC
    Django 遵循了经典的 模型-视图-控制器(Model-View-Controller)模式,但并没严格遵守。

  • 工作流程

    • 针对某个URL的HTTP请求进入
    • Django使用一些规则决定由哪个视图函数处理这个请求(这一步叫作解析URL)
    • 选中的视图函数处理请求,然后返回HTTP响应

    因此要测试两件事: 能否解析网站路径的URL,将其对应到编写的视图函数
    能否让视图函数返回一些HTML

  • 安全

    • 跨站请求伪造(Cross-Site Request Forgery, CSRF)漏洞
  • Django ORM

    • 对象关系映射器(Object-Relational Mapper)
    • ORM 的任务是模型化数据库 创建数据库其实是由另一个系统负责的,叫作迁移(migration),makemigraitons 创建迁移
  • 测试

    • factory_boy 自动创建模拟数据

待学习知识

部署 (deploy)

  • Solid Python Deployments for Everybody “Hynek Schalawack”
  • Git-based fabric deploys are awesome “Dan Bravender”
  • Two Scoops of Django # deploy content
  • The 12-facotr App

自动部署

  • fabric3
    允许在 Python 脚本中编写可在服务器中执行的名。这个工具很适合自动执行服务器管理任务
  • 把配置文件纳入版本控制
  • 自动配置
  • 配置管理工具
    • Salt
    • Puppet

前端

  • Bootstrap

    • LESS or Sass to customize bootstrap
  • 客户端打包工具

    • npm and bower

安全

  • fail2ban(服务器安全工具)

生产环境和开发环境中使用不同的密钥是个好习惯

生产服务器环境

  • 不要在生产环境中使用 Django 开发服务器,使用 Gunicorn 或者 uWSGI
  • 使用 Ningx 处理静态文件
  • 检查 settings.py 中只针对开发环境的设置
  • 安全性

使用Git标签标注发布状态 为了保留历史标记,使用Git标签(tag)标注代码库的状态,指明服务器中当前使用的是哪个版本

1
2
3
4
5
git tag LIVE
export TAG=$(date +DEPLOYED-%F%H%M) # 生成一个时间戳
echo $TAG # 会显示 “DEPLOYED-”和时间戳
git tag $TAG
git push origin LIVE $TAG #推送标签

Web 应用的验证可以放在两个地方:

  • 客户端(JavaScript或HTML5属性)
  • 服务器端
    在服务器端,对Django而言,也有两个地方可以执行验证:一个是模型层,一个是表单层,位置较高
    如果想检查某件事是否会抛出异常,可以使用self.asserRasies上下文管理器
    在渲染表单的视图中处理改视图接收到的Post请求

with 上下文管理器如何使用

  • 上下文管理协议(Context Manager Protocol)
    包含方法__enter__()__exits__(),支持该协议的对象要实现这两个方法

  • 上下文管理器(Context Manager)
    支持上下文管理协议的对象,这种对象实现了上述两种方法。上下文管理器定义执行with语句时要建立的运行时上下文,负责执行with语句块上下文中的进入与退出操作。

    1
    2
    with EXPR as VAR:
    BLOCK

Django URL 反向解析

  • 每个模型对象都对应一个特定的 URL,因此可以定义一个特殊的函数,命名为 get_absolute_url,其作用是获取显示单个模型对象的页面 URL

关于数据层验证

  • 数据库层验证是数据完整性的最终保障
  • 但是数据库层验证有失灵活性
  • 对用户不太友好(表单验证考虑到了用户,不会直接报错,而是显示友好的错误消息)

表单验证

  • 可以处理用户输入,并验证输入值是否有错误

  • 可以在模板中使用,用来渲染HTML input元素和错误消息

  • 表单甚至可以把数据存入数据库
    表单是放置验证逻辑的绝佳位置。

  • 前端开发 angular.js 和 React(MVC) 框架
    这些框架的教程大都使用一个 RSpec 式断言库,名为 Jasmine。MVC 框架,使用 Jasmine 比 QUnit 更方便

JavaScript测试

  • Selenium 最大的优势之一是可以测试 JavaScript 是否真的能使用,就像测试 Python 代码一样。
  • JavaScript 测试运行库有很多,QUnit 和 jQuery 联系紧密

主要的挑战

  • 管理全局状态。这包括:
    • DOM/HTML固件
    • 命名空间
    • 理解并控制执行顺序

无密码验证系统的原理是,只使用电子邮件地址确认身份

TDD: 模拟技术和测试隔离

创建 spike '探索' 分支,使用 django的mvc模式尽快测试功能是否可行,并创建相应的 版本分支, 测试可行后,回归到正式分支中, 
开始TDD流程:创建功能测试->单元测试(Js测试)->编写代码  
模拟技术: 测试外部依赖(例如邮件)的关键技术

  • 探究
    为了学习新API或调查新方案的可行性而做的探索性编程。没有测试也能探究。最好在一个新分支中去探究,去掉探究代码时再回到主分支。

  • 去掉探究代码
    把探究所得应用到实际代码中
    要完全摒弃探究代码,然后从头开始,用 TDD 流程再实现一次。去掉探究代码后实际编写的代码往往与最初有很大不同,通常会更好。

  • 针对探究代码编写功能测试
    该不该这么做要视情况而定。支持这么做的人人为,这样有助于正确编写功能测试–找出测试探究的方法与探究本身一样具有挑战性;不支持这么做的人觉得这样 会阻碍思路,写出的代码往往与探究时很像 – 我们要力求避免这种情况。

    使用测试数据预先填充数据库的过程,例如存储 User 对象及其相关的Session对象,叫做设置“测试固件”(test fixture)


显式等待辅助方法最终版:wait装饰器
不同测试类型以及解耦 ORM 代码的利弊 集成测试(integration test)和整合测试(integrated test)的区别:

  • 功能测试(验收测试、端到端测试)

    • 从用户的角度出发,最大程度上保证应用可以正常运行
    • 但是反馈时间用时长
    • 无法帮助我们写出简介的代码
  • 整合测试(依赖于ORM或Django测试客户端等)

    • 编写速度快
    • 易于理解
    • 发现任何集成问题都会提示
    • 但是,并不总能得到好的设计
    • 一般运行速度比隔离测试慢
  • 隔离测试(使用mock等模拟)

    • 涉及的工作量最大
    • 可能难以阅读和理解
    • 但是,这种测试最能引导你实现更好的设计
    • 运行速度最快

解耦应用代码和 ORM 代码 力求隔离测试的后果之一是,我们不得不从视图和表单等处删除ORM代码,把它们放到辅助函数或者辅助方法中。 如果从解耦应用代码和ORM代码的角度看,这么做有好处,还能提高代码的可读性。当然,所有事情都一样,要结合实际情况判断是否值得付出额外精力去做。


由外而内的TDD

  • 由外而内的 TDD
    一种编写代码的方法,由测试驱动,从外层开始(表现层,GUI),然后逐步向内层移动,通过视图层或控制器层,最终达到模型层。 这种方法的理念是由实际需要使用的功能驱动代码的编写,而不是在底层猜测需求。
  • 一厢情愿式编程
    总是为还未实现的功能编写测试。
  • 由外而内的缺点
    这种技术鼓励我们关注用户立即就能看到的功能,但不会自动提醒我们为不是那么明显的功能编写关键测试, 例如安全相关的功能。需要记得编写这些测试。

持续集成

持续集成(Continuous Integration)

  • CI 服务器:Jenkins

  • 尽早为自己的项目搭建CI服务器
    一旦运行功能测试所花的时间超过几秒钟,应该将这个任务交给CI服务器,确保所有测试都能在某处运行。

  • 测试失败时截图和转储 HTML 如果能够看到测试失败是网页是什么样,对调试更加便利。截图和转储 HTML 有助于调试 CI 服务器中的失败,而且对本地运行的测试也很有用

  • 时刻准备调整超时 CI 服务器的运行速度可能没有本机快,尤其是同时运行多个测试、负载较高时。要时刻准备调整超时,设置它为较大值,尽量降低随机失败的概率

  • 想办法把 CI 和过渡服务器连接起来
    使用 LiveServerTestCase 的测试在开发环境中不会遇到什么问题,但若想得到十足的保障,就要在真正的服务器中运行测试。 想办法让 CI 服务器把代码部署到过渡服务器中,然后在过渡服务器中运行功能测试。这么做还有个附带好处:测试自动化部署脚本是否可用。


页面模式

减少功能测试中重复代码的方式,叫做“页面对象” 页面模型背后的思想是: 把网站中某个页面的所有信息都集中放在一个地方,如果以后想要修改这个页面,比如简单的调整 HTML 布局,功能测试只需改动一个地方,不用到处修改多个功能测试

  • 可预见的变化
    通常难以预测需求变化,但是根据现有需求预见将来的需求变化并不是不可能
  • 不可预见的变化
    但是可预见的变化并不一定会发生,有时甚至永远不会发生,还有的会因为某种原因转化为

使用常识,同时需要明白事情是会发生变化的,因此需要注意现有任务的优先级。

夹具(fixture): 为测试准备的一系列的对象状态。


重构

精髓: 小步修改,每次修改后就运行测试。

手法

临时整理:

  • 以查询取代临时变量
    • 检查传入参数是否是计算后获得
    • 临时变量,且不再被修改。
  • 函数声明
    • 函数内部使用新提炼的函数
    • 删除参数
    • 修改函数名称
  • 以子类取代类型码
    • 引入子类,弃用类型代码
  • 工厂函数取代构造函数‘’
  • 以多态取代条件表达式

整理完成:

  • 拆分循环: 分离出累加过程
  • 移动语句: 将累加变量的声明与累加过程集中到一起
  • 提炼函数:提炼出计算总数的函数
    • 检查提炼到新函数后,哪些变量会被修改(在函数中直接返回,也可以在函数中初始化),哪些变量不会被修改(以参数方式传递)
    • 函数是否能进一步提升表达能力
      • 修改变量名,使其简洁
      • 函数参数重命名
  • 内联变量(inline variables):完全移除中间变量

为原函数添加足够的结构,以便能更好的理解,看清它的逻辑结构。 复杂的代码块分解为更小的单元,与好的命名一样重要

实现复用:

  • 拆分阶段
  • 搬移函数
  • 以管道取代循环

重构过程的性能问题: 大多数情况下可以忽略它。如果重构引入了性能损耗,先完成重构,再做性能优化。

坏味道:

  • 命名不准确(mysterious name)

    • 函数声明(用于给函数改名)
    • 变量改名
    • 字段改名
  • 重复代码(duplicated code)

    • 提炼函数(提炼重复的代码)
      • 移动语句(将相似的部分放在一起一边提炼)
    • 函数上移
  • 过长函数(long function)

    • 提炼函数
    • 以查询取代临时变量(消除临时元素)
    • 引入参数对象
    • 保持对象完整
    • 以命令取代函数
    • 分解条件表达式
    • 以多态取代条件表达式
    • 拆分循环
  • 过长参数列表(long parameter list)

    • 以查询取代参数
    • 保持对象完整
    • 引入参数对象
    • 移除标记参数
    • 函数组合成类(将共同的参数变成类的字段)
  • 全局数据(Global data)

    • 封装变量(用一个函数包装起来)
  • 可变数据(mutable data)

    • 封装变量(确保数据更新操作都通过很少几个函数来进行)
    • 拆分变量(拆分为各自不同用途的变量,从而避免危险的更新操作)
    • 移动语句 + 提炼函数(把逻辑从处理更新操作的代码中搬移出来)
    • 查询函数和修改函数分离(针对 API,确保调用这不会调到有副作用的代码,)
    • 移除设值函数
    • 以查询取代派生变量
    • 函数组成类
    • 函数组成变换
    • 将引用对象改为值对象
  • 分散式变化(divergent change)

    • 拆分阶段
    • 搬移函数(将处理逻辑与数据分开)
    • 提炼函数 + 搬移函数(将函数内部混合的处理逻辑分开)
    • 提炼类
  • 散弹式修改(shotgun surgery) *

移除局部变量的好处就是做提炼时会简单得多,因为需要操心的局部作用域变少了。

代码的实际运行路径?

可选择的编程方式:

  • 将函数的返回值命名为 “result”

  • mock

    • 使用驭件测试外部依赖,达到隔离外部副作用
    • 使用驭件同时可以减少重复

url -> middle -> auth -> view -> model