总纲
差的测试会增加维护的负担,好的测试才能指导开发。品质差的测试可能让开发慢如蜗牛。
测试需要达到目的的同时尽量保证不要给重构及变更带来阻碍。
测试
测试的粒度:单元测试 -> 集成测试 -> 验收测试(用户场景测试)
单元测试使我们优化内部品质:模块能独立于系统运行,说明其边界是清晰的,高内聚,低耦合的,更可能在其他地方复用。验收测试保证外部品质。
TDD四步循环:失败 - 报告 - 通过 - 重构
测试的好处:
- 确定要实现的目标
- 通过测试可以发现设计缺陷
- 有助于促进上下文无关性
- 促进通过分解、萌芽、打包来发现值类型和对象
起步
- 新项目:做最少的决定,实现可行走的骨架,尽早暴露不确定性,启动TDD循环 -> 从反馈中学习。
- 现有代码:回归测试 -> 单元测试 -> 改动。
设计
- 对象间的关系:依赖、通知、调整
- 封装:确保通过API影响对象的行为;隐藏:实现方式不可见
- 组合比部分之和更简单
- 上下文无关性
CRC卡进行辅助设计。
好的设计:
- 根据角色进行命名,而不是根据实现
- 使用小的方法来去掉语法噪声,更好的表达意图,最后达到跟读文章一样的效果
- 分离并发策略与功能
- 事件源外部化,不使用内部的定时器
编写好的测试
好的单元测试:
- 针对行为进行测试而不是方法
- 更愿意读测试而不是代码
- 单个测试规模小,容易理解
- 依赖在对象构造时传入
- 模拟接口而不是模拟具体的类
- 少量的预期
- 准确指定应该发生什么,没有多余的指定
坏味道:
- get查询方法暴露内部状态,破坏了封装
- 对象名字出现连词(与、或、但是),通常可以重构以抽取对象
- 接口以I打头,或者出现xxImpl:意味着实现不能很好的命名,实现包含领域信息,接口定义角色,总是有可能将其分开
- 模拟值类型
- 模拟一个无法替换的对象:单例是依赖关系、隐式依赖也是依赖
- 构造方法太大,测试很难编写:可能可以萌芽或者打包新的类型;关注点不集中,可能需要分解为新的类型;对通知关系和调整关系使用默认值,提供方法修改它们
- 测试中预期太多:测试的重点难以发现
- 测试中存在很多与重点无关的代码,如用try catch块捕捉异常
- 测试中存在具体值如null,应该为其命名以明确其含义
- 测试出错难以定位问题:测试描述不清楚
编写测试:
- 编写辅助对象进行测试,如FakeAuctionServer和测试数据建造者
- 使用自描述的值(覆盖toString),或者明显的预装值(Integer.MAX_VALUE)
- 忽略不相关的对象
- 明确事务边界、使用压力测试来测试线程问题
- 使用捕获通知来探测变化以测试异步功能,使用waitUntil assertEventually来检测状态变化
- 提取测试结构是注意不要让测试代码太抽象,无需像对待产品代码那样重构测试,测试代码的最高目标是让测试描述目标代码做了什么
- 如果类需要创建内部支持对象,在测试中该对象应该是不可见的,而不是mock它
其他:
重构:
- 为潜在的对象命名,因为某个东西有名字了,就可以控制它了
日志:
- 支持性日志是一项功能,是应用程序用户接口的一部分,可以用测试驱动
- 诊断性日志用于开发者发现问题,可以不用测试驱动,可以使用通知而不是记日志实现