《测试驱动开发》
测试
读书笔记
前言
代码简洁可用(clean code that works),是测试驱动开发所追求的目标。
原因
- 它是一个可预测的开发方法。你知道什么时候可以完工,而不用去担心是否会长期被bug困扰;
- 它给你一个全面正确地认识和利用代码的机会。如果你总是草率地利用你最先想到的方法,那么你可能再也没有时间去思考另外一种更好的方法;
- 它改善了你的软件用户的生活;
- 它让软件开发小组成员之间相互依赖;
- 这样的代码写起来感觉很好;
测试驱动开发中的做法:
- 只有自动测试失败时,才重写代码;
- 消除重复设计,优化设计结构;
开发过程中所经历的阶段:
- 不可运行 - 写一个不能运行的测试程序,一开始这个测试程序甚至不能编译;
- 可运行 - 尽快让这个测试程序工作,为此可以在程序中使用一些不合情理的方法;
- 重构 - 消除在让测试程序工作的过程中产生的重复设计,优化设计结构;
第一部分 资金实例
1 多币种资金
建立美元类(成员amount),并实现times操作
建立todo-list,按照清单逐一实现并删除
- 创建一个清单,列出我们所知道的需要让其运行通过的测试;
- 通过一小段代码说明我们希望看到怎么样的一种操作;
- 暂时忽略JUnit的一些细节问题;
- 通过建立存根(stub)来让测试程序通过编译;
- 通过一些另类的做法来让测试运行通过;
- 逐渐使工作代码一般化,用变量代替常量;
- 将新工作逐步加入计划清单,而不是一次全部提出;
2 变质的对象
修改实现times操作
尽快使测试程序可运行的三条策略:
- 伪实现 - 返回一个常量并逐渐用变量代替常量,直至伪代码成为真实实现的代码;
- 显明实现(obvious implementation) - 将真实的实现代码键入;
- 三角法 - 在具有多个实例的时候对代码进行一般化;
做法:
- 将一个设计缺陷(副作用)转化为一个由此缺陷导致运行失败的测试程序;
- 采用存根实现使代码迅速编译通过;
- 键入我们认为正确的代码以使测试程序能尽快工作;
3 一切均等
实现美元equals方法,并times操作返回新对象
数值对象:数值对象的实例变量值在构造函数中被指定,以后不允许发生变化,不需要担心别名问题,同时隐含了所有操作需要返回新对象。
4 私有性
将amount属性设置为私有
5 法郎在述说
拷贝美元类实现法郎类
6 再谈一切均等
消除重复代码,将公共代码部分移动到父类
为法郎和美元建立父类Money,将equals等操作移动到父类中。
7 苹果与桔子
法郎和美元相比较相等,修改Money的equals方法,使其先行判断类是否一样
8 制造对象
法郎和美元的times函数完全一致,使其times函数都返回Money,将美元和法郎的构造函数改为Money类下的工厂函数,Money.dollar/Money.franc,Money设置为抽象类
9 我们所处的时代
- Money中添加接口,currency(),具体子类中添加对应的string currency;
- 将子类的string currency构造字符串方法移动到静态工厂方法中;子类直接调用super(amount, currency)
10 有趣的times方法
- times上移到Money类, Money(amount*times, currency)
11 万恶之源
美元与法郎只剩下构造函数,删除美元与法郎类
12 加法,最后的部分
引入Expression类来表示货币加法的和
通过Bank.reduce(exp, ‘USD’)来将其转回Money类
13 完成预期目标
定义Expression子类Sum用以表示Money相加
Money与Expression分别添加reduce函数,为bank.reduce调用
14 变化
将bank作为参数传递给Money和Expression的reduce函数
bank提供rate函数用以提供汇率,提供addrate函数添加汇率
15 混合货币
将Money类一般化为Expression类,Expression中添加plus
16 抽象,最后的工作
完成所有代码
17 资金实例回顾
测试质量:
- 性能测试
- 压力测试
- 可用性测试
- 语句覆盖率:理应100%
- 缺陷插入:改变一行代码,看测试能不能通过
第二部分 xUint实例
- 实现了TestCase类,包含run/setup/teardown等方法
- 实现了TestSuite类,其有列表成员用以保存TestCase,run函数递归调用TestCase的run方法
18 步入xUnit
19 设置表格
20 后期整理
21 计数
22 失败处理
23 如何组成一组测试
24 xUnit回顾
第三部分 测试驱动开发的模式
25 测试驱动开发模式
- 测试
- 相互独立的测试(Isolated Test)
- 所运行的各种测试之间应该怎么样相互影响呢?没有任何相互影响
- 测试列表(Test List)
- 你应该测试什么呢?在开始写测试之前,写一个包含所有你认为必须要编写的测试的清单
- 测试优先(Test First)
- 你应该在什么时候编写测试呢?在你编写要被测试的代码之前
- 断言优先(Assert First)
- 什么时候应该写断言?试着一开始就写断言
- 从哪儿开始构建一个系统?从我们对最终系统的描述开始
- 从哪儿开始编写一项功能?从我们希望最终代码能够通过的测试开始
- 从哪儿开始写一个测试?从测试完成时能够通过的断言开始
- 写一个测试,实际上在解决如下问题:
- 这一功能属于哪一部分呢?是通过修改现有方法得到,还是在现有类中构造一个新的方法?或者是在一个新的地方来实现现有的方法?或是实现一个新的类来得到?
- 名字应该怎么起呢?
- 你怎样去检查结果的正确性?
- 什么是正确的结果?
- 这个测试是否需要其他的测试?
- 测试数据(Test Data)
- 使用容易让人理解的数据,是给大家写测试,而不是编排数据
- 使用真实数据
- 在使用根据实际运行所采集到的外部事件序列来测试实时系统时
- 在将目前系统的输出与以前系统的输出进行匹配时(平行测试)
- 当你对某种仿真系统进行重构而期望在完成时得到完全相同的结果时,特别是在浮点精度有可能存在问题的时候
- 显然数据(Evident Data)
让测试自身包含预期的和实际的结果,并且努力式它们之间的关系明显化
26 不可运行状态模式
- 一步测试(One Step Test)
- 启动测试(Starter Test)
- 从测试某个实质上不做任何工作的操作开始
- 如果从真实的测试开始,会需要解决一连串的问题,从一个真实测试开始会使你在很长时间内都得不到反馈
- 这个操作隶属于哪儿
- 正确的输入是什么
- 有了这些输入以后正确的输出是什么
- 说明测试(Explanation Test)
如何拓展自动测试呢? 利用测试来请求以及提供说明解释。通过测试来说明代码要完成的内容以及边界条件
- 学习测试(Learning Test)
当使用新类的新方法时,可以通过为其写测试用例来学习其使用方法
- 回归测试(Regression Test)
针对错误,写一个尽可能小的会失败的测试,一旦运行,就可以对其进行修缮
27 测试模式
- 子测试(Child Test)
- 写出能够代表大部分大的测试例程的小测试例程,让这些小的测试例程运行通过,然后再处理大的测试例程
- 测试太大的时候问自己:为什么它会这么大?我应该采取什么不同的方法使它变小一些?现在我感觉如何?
- 模拟对象(Mock Object)
- 如何测试一个依赖于昂贵且复杂的资源的对象?创建一个这些资源的模拟版本
- 模拟对象鼓励你仔细思考个个对象的可见性,减少设计中的耦合
- 自分流(Self Shunt)
- 如何测试对象间是否正常交互?让测试对象与测试用例而不是期望的对象进行交互
- 自分流要求使用提取接口(Extract Interface)来实现接口
- 日志字符串(Log String)
- 如何测试才能使消息调用序列是正确的?将日志保存到字符串中,当调用一个消息时将日志写到字符串尾
- 不关心顺序,则可以使用字符串集合进行保存和比较
- 清扫测试死角(Crash Test Dummy)
- 如何测试到不大可能被调用到的错误代码呢?使用一种特殊的对象调用它,这个对象抛出一个异常而不做任何实际工作
- 清扫测试死角就像模拟对象,但是不需要模拟整个对象,Java的匿名内部类可以简便实现要模拟方法
- 不完整测试(Broken Test)
- 离开工作前使测试不完整,则再次工作时易于找到上次未完部分
- 提交前保证所有测试运行通过
28 可运行状态
- 伪实现(直到你成功)
- 测试不能通过时首先执行什么?返回一个常量,一旦运行起来后,该常量逐渐转换成变量表示的表达式
- 三角法(Triangulation)
- 怎么样可以更适当地利用测试推动抽象呢?只有当你由两个或者两个以上的例子时,你才能进行抽象
- 显明实现(Obvious Implementation)
- 从一到多(One to Many)
- 怎么样实现一个作用于对象集合体的操作呢?首先在非集合体中实现,然后使之作用于集合体
29 xUnit模式
- 断言(Assertion)
- 怎样检验测试是否正确工作?写一个布尔表达式对代码是否工作自动作出判断
- 将对象当做黑盒是困难的(只使用公共规程,外部接口)
- 希望使用白盒测试不是一种测试问题,而是一种设计问题
- 固定设施(Fixture)
- 怎样创建几个测试都需要的通用对象呢?把测试中的局部变量转变为实例变量,重载SetUp并初始化这些变量
- 外部固定设施(External Fixture)
- 如何在固定设施中释放外部资源呢?覆盖TearDown,然后释放资源
- 测试方法(Test Method)
- 怎样表示一个单一的测试用例呢?把它看着一种方法,并且其名字以“test”开头
- 类用以表示固定设施,而测试自然归属为方法
- 异常测试(Exception Test)
- 怎样测试期望的异常呢?捕获每个期望的异常,然后忽略它们,只有当没有抛出异常时才报错
- 全部测试(All Tests)
- 怎样一次性执行所有的测试呢?把所有的测试套件(Suite)合成一个套件 - 每个包一个,而整个应用是一个集成的测试包
30 设计模式
- 命令(Command)
- 值对象(Value Object)
- 通过创建其值一经创建便永远不改变的对象来避免别名问题
- 空对象(Null Object)
- 模板方法(Template Method)
- 使用可以通过继承来具体化的抽象方法来表示计算序列中不变的内容
- 插入式对象(Pluggable Object)
- 通过调用另一个具有两种或两种以上实现的对象来表示变化的内容
- 插入式选择器(Pluggable Selector)
- 工厂方法(Factory Method)
- 冒名顶替(Imposter)
- 递归组合(Composite)
- 收集参数(Collecting Parameter)
31 重构
- 调和差异(Reconcile Differences)
- 隔离变化(Isolate Change)
- 数据迁移(Migrate Data)
- 提取方法(Extract Method)
- 内联方法(Inline Method)
- 提取接口(Extract Interface)
- 转移方法(Move Method)
- 方法对象(Method Object)
- 添加参数(Add Parameter)
- 把方法中的参数转变为构造函数中的参数
32 掌握TDD