测试驱动的开发学习笔记
邱志军 于 2008年 3月 7日起
2008 年3月9日 第一版
赛尔软件有限公司(筹)
第I部分 资金实例
通过资金实例迅速了解TDD测试驱动的开发的过程,这个过程大体上可以归纳为以下几个步骤:
1. 快速新增一个测试
2. 运行所有测试,发现最新的测试不能通过
3. 做一些小小的改动
4. 运行所有的测试,并且全部通过
5. 重构代码,以消除重复设计,优化设计结构
开始进行TDD时,我们容易产生以下一些疑问:
l 每个测试是怎样覆盖一些新增功能的
l 为了使测试通过,我们所做的改动有多小,方法有多笨
l 多长时间运行一次测试
l 重构是由多么微小的步骤组成
第一章 多币种资金
依赖关系与重复设计
测试程序与代码所存在的问题不再重复设计而在依赖关系——你不可能只改动其中一个而不改变另外一个。但是,如果问题出在依赖关系上,那么其表现就是重复设计。因此,只有在编写下一个测试程序之前消除现有的重复设计,通过一处且仅仅一处的改动即可让下一个测试运行的可能性就越大。
一个完整的测试周期
1. 创建一个清单,列出我们所知道的需要让其运行通过的测试
2. 通过一小段代码说明我们希望看到怎样的一种操作
3. 暂时忽略Junit的一些细节问题
4. 通过建立存根来让测试程序通过编译
5. 通过一些另类的做法来让测试运行通过
6. 逐渐使工作代码一般化,用变量替代常量
7. 将新工作逐步加入计划清单,而不是一次提取全部
第二章 变质的对象
尽快使测试程序可运行的三条策略
l 伪实现——返回一个常量并逐渐用变量代替常量,直到伪实现的代码成为真实实现的代码
l 显明实现——将真实实现的代码键入
l 三角法——只有当例子达到两个或以上的时候才对代码实施一般化
测试程序的演进过程
1. 将一个设计缺陷(副作用)转化为一个由此缺陷导致运行失败的测试程序
2. 采用存根实现使代码迅速编译通过
3. 键入我们认为正确的代码以使测试程序能尽快工作
第三章 一切均等
使用数值对象的有一个巨大的优势:你不用担心别名问题。数值对象的一个隐含的意思是所有的操作都必须返回一个新的对象,另一个隐含意思是必须实现equals()方法。
再议三角法
l 仅在完全不知道如何重构的情况下才使用该方法
l 如果对设计方案一点思路都没有,三角法则提供了提个从稍微不同的角度思考问题的机会
三角法测试周期
1. 注意到我们的设计模式(VO),隐含了一种操作
2. 测试这种操作
3. 给它一个简单的实现
4. 并不立即重构,而是进一步测试
5. 通过重构一次性解决两个测试用例所引入的重复设计
第四章 私有性
测试测序如果直接引用工作代码中的字段,容易导致测试程序同代码间的耦合(其实质就是同客户代码的耦合)。如果能够通过代码新的操作消除这种依赖,那么就能够优化代码的设计——其实这也是由共有字段导致的副作用激发新的测试的例子。
测试改进的一般过程
1. 使用刚刚开发的功能来改进一个测试
2. 观察到如果两个测试同时失败,那么问题就严重了
3. 尽管有风险,但还是要继续前进
4. 使用测试中对象的新功能以减少测试程序同代码之间的耦合度
第五章 法郎在诉说
不同的阶段有不同的目的,需要不同的解决方式和审美观。
渐进的测试演化
1. 无法完成一个大的测试,所以我们会首先通过一个小的测试先行动起来
2. 无所顾忌的通过复制和编辑来写出这个测试程序
3. 更糟糕的是,通过将整个模型代码拷贝过来并加以编辑来让测试程序工作
4. 自我保证在重复设计消除前绝不回家
第六章 再谈一切均等
基于测试消除重复设计
1. 逐步将一个类中的公共代码部分移至父类
2. 将第二个类也作为父类的子类
3. 使两个equals函数一致然后在消除重复的一个
第七章 苹果和橘子
基于测试解决设计问题
1. 着手解决一个困扰我们的难题并将它转化为一个测试程序
2. 用一种合理但并不完美的方法使测试程序运行通过
3. 除非有更好的动机,否则不要引入更多的设计
第八章 制造对象
消除测试程序同子类对象存在的耦合,可以自由的改变继承关系而不会对模型代码造成任何影响。
基于测试解耦测试程序和模型代码
1. 通过使同一方法的两个变种的签名相一致,我们朝着消除重复设计的方向又前进了一步
2. 至少将这个方法的一个声明移至共同的父类中
3. 通过引入工厂方法消除测试程序同具体子类的耦合
4. 当子类被消除后(在测试中),有些测试将是冗余的,但是我们暂时不管他们
第九章 我们所处的时代
TDD中的重构准备
1. 大的设计构思受阻,所以我们着手解决前面所发现的小问题
2. 通过将方法变体上移至工厂方法,使构造函数一致
3. 打断重构,在times方法中应用工厂方法
4. 用一大步做同样的重构
5. 将相同的构造函数上移
第十章 有趣的Times()方法(略)
第十一章 万恶之源
总结
l 在掏空了子类的功能后,将其删除
l 消除了对旧的代码结构有意义的、但对新的代码结构多余的测试
第十二章 加法、最后的部分
隐喻在TDD中的应用
l 把一个大的测试削减为一个小一些的测试,仍然算是在前进
l 认真思考与我们的计算有关的比喻
l 基于我们新的比喻,重写了之前的测试
l 让测试尽快通过编译
l 让测试运行通过
l 带着一丝惶恐、期待着为写出真实的实现而必须进行的重构
第十三章 完成预期目标
TDD中的启发式探索
1. 因为重复设计没有完全消除,所以没有把一个测试标记为完成
2. 为了知道如何实现,继续往前走而不是往回走
3. 写一个测试以迫使创建一个我们稍后要用到的类
4. 开始更快的实现
5. 在一个地方采用强制类型转换来实现代码,然后一旦代码测试通过,就把它移到本该属于的位置
6. 为了消除显示的类判定而引入多态
第十四章 变化
本章总结
1. 用了很短的时间,增加了一个希望会用到的参数
2. 分离测试程序同代码的数据重复
3. 编写测试程序核实一个JAVA操作
4. 引入了一个私有帮助类,但未进行测试
5. 在重构中犯了一个错误,我们使用简陋的办法迅速处理,并通过写测试程序单独考虑这一问题
第十五章 混合货币
TDD中的渐进抽象
1. 编写我们想要的抽象,然后暂且通过简陋的方法让它快速运行
2. 从叶子再到根(测试程序),来实行一般化
3. 根据编译器的提示进行修改,这一改动会引起一系列的连锁改动
第十六章 抽象、最后的工作(略)
第十七章 资金实例回顾
第II部分(略)
第十八章 步入Xunit
第III部分 测试驱动开发的模式
第二十五章 测试驱动开发模式
TDD的一些基本策略
l 测试是什么意思?
l 什么时候进行测试?
l 如何选择要测试的逻辑?
l 如何选择要测试的数据?
测试(名词)
l 你如何测试自己的软件?编写自动测试程序
l 测试是一个动词,意识是“评估”
l 测试还是一个名词,意思是:导致最终是接受还是不接受的过程
l 编码后再测试容易导致压力渐增的正反馈循环,最终放弃测试而崩溃
相互独立的测试
l 所运行的各种测试应该怎样互相影响?没有任何相互影响
l 如果一个测试失败了,那么它就对应且仅对应一个问题
l 相互独立的测试也意味着所有的测试都是不依赖于顺序的
l 相互独立的测试的第二个含义:让你的问题分解为一些彼此正交的小问题,这样每个测试建立环境更加简单、快捷。
l 独立测试鼓励你根据高内聚、松耦合的对象组合出解决方案
测试列表
你应该测试什么呢?在开始测试之前,写一个包含你所能想得到的必须要编写的测试的清单。
1. 把想在以后几个小时之内完成的任何事情都记录在电脑旁的一张纸上
2. 将一周或一个月的工作计划,列出一张清单钉在墙上
在TDD中,我们记录在列表上的就是我们要去实现的测试,清单的应用过程如下:
1. 把你所知道的需要实现的每种操作的范例都记录在清单上
2. 对于那些目前还不存在的操作,将其空版本记录在清单上
3. 列出所有你认为在这一轮编程结束后为了获得整洁的代码所需的重构
测试优先
应该在什么时候编写测试呢?在你编写要被测试的代码之前。如果我们先进行测试,我们的压力就会减少,这样使得我们更乐意去测试,测试处于一种良性循环中。测试是一种考虑设计的方式,控制规模的方法,通过测试逐步将功能实现。
断言优先
我们什么时候开始写断言?试着一开始就编写断言。你难道不喜欢自身相似性(self-similarity)吗?
l 我们应该从哪儿构建一个系统?从对最终系统的描述开始
l 我们应该从哪儿开始编写一项功能?从我们希望最终代码能够通过的测试开始
l 我们应该从哪儿开始写一个测试?从测试完成时能够通过的断言开始
当你在写一个测试时,你是在同时解决好几个问题, 即使你不再需要考虑具体的实现细节。
这个功能属于哪个部分?在何处实现新的方法?
名字应该怎么取
你应该怎样检查结果的正确性?
什么是正确的结果?
这个测试是否需要其他的测试?
测试数据
我们在测试优先的测试中应该使用什么数据呢?使用那种容易让人理解的数据。测试数据的一个诀窍是:永远不要用一个常量来表达多个意思?另一种测试数据是真实数据。
显然数据
你如何表达数据的意图?让测试自身包含预期的和实际的结果,并努力使他们之间的关系明显化。显然数据有利的一面是它使编程更容易,只要我们在断言中把表达式写清楚了,就知道要写什么程序了。
第二十六章 不可运行状态模式
这些模式是关于你什么时候写测试,在哪里写测试,以及什么时候停止写测试的。
一步测试(One Step Test)
你将从计划列表中选择编写哪一个测试呢?选择那个具有指导意义且有把握实现的测试。每个测试都应当代表迈向总体目标的一步。
启动测试(Starter Test)
我们应该从那个测试开始呢?从测试某个实质上不做任何工作的操作开始。启动测试通常比较高阶,更像是一个应用测试。启动测试相比真实测试更容易满足一次只解决一个问题的原则。
说明测试(Explaination Test)
如何拓展自动测试呢?我们通过测试来请求及提供说明解释。
学习测试(Learning Test)
你什么时候为外部软件编写测试呢?在你第一次准备应用这个包中的某项新功能时。编写学习测试乃是例行工作,如果测试不能运行,那么运行应用就没有意义了。
另外的测试
如何才能让技术讨论不跑题呢?当出现与当前话题不直接相关的想法时,那么就在列表中新添一个测试,然后返回正题。
回归测试(Regression Test)
当一个错误被发现时,你想做的第一件事是什么呢?写一个尽可能小的测试,一旦运行,就对其加以修缮。
休息(略)
重新开始
当你感到迷失方向时,该怎么办?扔掉原来的代码,重新开始。
便宜的桌子,舒适的椅子
为了测试驱动开发,你需要什么条件呢?买个舒适的椅子,至于其他则能省就省。
第二十七章 测试模式
以下模式是编写测试代码细节的模式。
子测试(ChildTest)
如果测试例程太大,你如何能让他运行起来?写出能够代表部分大的测试例程的小测试,让这些小测试例程运行通过,然后再处理大的测试例程。不可运行/可运行/重构的节奏对于持续的成功非常重要。
模拟对象(Mock Object)
如何测试一个依赖昂贵且复杂的资源的对象?创建一个这些资源的模拟版本。典型的例子就是数据库,大多数测试写一个数据库一样的对象,但他仅仅驻留在内存中。模拟对象鼓励你仔细思考个个对象的可见性,消除设计中的耦合。
自分流(Self Shunt)
如何测试对象间是否正常交互?让测试对象与测试用例而不是期望的对象交互。
日志字符串
如何测试才能使消息调用序列是正确的?将日志保存在字符串中,当调用一个消息时,就向字符串尾追加相应的信息。
清扫测试死角
如何测试到不大可能被调用到的错误代码呢?使用一种特殊的对象调用它,这个对象抛出一个异常而不做任何操作。
不完整测试
当你独自编程时,如何能离开编程工作一段时间呢?使剩下的测试不完整。
提交前确保所有的测试运行通过
当你在团队中编程时如何结束一段编程工作?让所有的测试运行起来。
第二十八章 可运行模式
一旦出现未能通过的测试,你就必须去处理。
伪实现(直到你成功)
测试不能通过时首先应该执行什么?返回一个常量。一旦你使测运行起来,那个常量就会逐渐转化为变量表示的表达式。使伪实现强有力的两个因素:
l 心理元素——得到可运行的状态与得到不可运行的状态的感觉截然不同
l 范围控制——程序员往往擅长想像各种各样将来的问题
三角法
怎样可以更恰当的利用测试推动抽象呢?只有当你有两个或以上的例子时,你才能进行抽象。三角法吸引人的地方在于他的规则看起来十分清楚,但导致了一个无穷循环。只有当我们确实不能确定关于计算的正确抽象时,才使用三角法。否则,我们就用伪实现或是显明实现。
显明实现
怎样实现简单的操作呢?直接实现。
从一到多
怎样实现一个作用于对象集合的操作呢?首先在非集合体中实现,然后使之作用于集合体。在实现过程中存在一个单个对象参数到对象集合参数的转变,转变中通过参数的增减隔离测试程序的变化。
第二十九章 xUnit模式
本章讲述测试框架的应用模式。
断言(Assertion)
怎样检测测试是否正确工作呢?写一个布尔表达式对代码是否工作自动作出判断。此外,Junit中利用断言中的第一个可选参数提供出错时的提示。
固定设施(Fixture)
怎样创建几个测试都需要的对象呢?把测试中的局部变量转化为实例变量,重写setUp()方法并初始化实例变量。
外部固定实施(External Fixrure)
如何在固定实施中释放资源呢?覆盖tearDown()方法,然后释放资源。
测试方法
怎样表示一个单一的测试用例呢?把它看作一个方法,并且其名字以test开头。
异常测试
怎样测试期望的异常呢?捕获每个期望的异常然后忽略它,只有当没有抛出异常时才报错。
全部测试
怎样一次执行所有的测试呢?把所有测试套件合成一个套件——每个包一个,而整个应用是一个集成的测试包。
第三十章 设计模式(略)
测试驱动的开发中要求我们从略微不同的角度来看待设计模式。
第三十一章 重构(略)
第三十二章 掌握TDD(略)