书籍连接: book.douban.com/subject/259…数据库
这本书教你为何要关注可测试性, 如何编写可测试的代码, 以及如何推进测试落地.bash
书中的代码是.NET的, 不太习惯, 后文中我改为用Golang进行举例.网络
第一部分是入门章节, 告诉咱们什么是单元测试, 为何要用单元测试, 如何写好单元测试.架构
什么是单元测试? 单元测试是一段自动化的代码, 这段代码调用被测试的工做单元, 以后对这个单元的单个最终结果的某些假设进行检验. 这里须要注意一个概念: 单元. 从调用系统的一个公共方法到产生一个测试可见的最终结果, 其间这个系统发生的行为总称为一个工做单元.框架
一个相关的概念是集成测试. 单元测试和集成测试的最主要区别在于: 是否使用了真实依赖物, 如数据库, 网络链接等等.函数
什么是好的单元测试? 优秀单元测试应该有以下特性:单元测试
在开发过程当中应该什么时候编写单元测试? 这个见仁见智, 能够采用TDD先写测试后写功能, 也能够写完功能再补测试.测试
单元测试的目的在于使得代码可维护. 若是你的单元测试没有促进这一目标, 就要反思是否是真的写好了单元测试.ui
核心技术章节主要介绍了如何使系统与外部依赖项隔离, 从而进行去依赖的单元测试.编码
一般咱们开发的系统都会有各类外部依赖项. 外部依赖项是系统中的一个对象, 被测试代码与这个对象发生交互, 但你不能控制这个对象. 常见外部依赖项包括文件系统, 线程, 内存以及时间等. 注意: 一旦你的系统中引入的真实的外部依赖项, 那么你进行的就是集成测试, 而非单元测试.
显然, 若是无法控制外部依赖项的行为, 就没法保证单元测试运行结果的稳定性. 那么如何使外部依赖项可控? 答案是使用伪对象 (fake) 替代真实的外部依赖对象.
那么接下来的问题是, 如何用伪对象替代外部依赖? 只须要找到被测试单元使用的外部接口, 而后将接口的底层实现替换成你能控制的代码. 若是这个接口与被测试单元直接相连, 就添加一个间接层, 隐藏这个接口. 来看下面这个例子:
// 判断文件是否存在
func IsFileExist(fileName string) bool {
_, err := os.Stat(fileName)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return false
}
复制代码
这段代码引入了文件系统依赖, 须要用一个伪对象替代真实文件系统. 然而, 文件系统有关的代码已经写死在函数里了, 这就须要引入一个中间层, 抽象出文件系统的操做. 这里咱们声明一个IFileManager接口:
type IFileManager interface {
IsFileExist(string) bool
}
复制代码
提供一个真实的文件系统实现和一个伪对象实现:
type PureFileManager struct{}
func (t *PureFileManager) IsFileExist(fileName string) bool {
_, err := os.Stat(fileName)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return false
}
type FakeFileManager struct{}
func (t *FakeFileManager) IsFileExist(fileName string) bool {
return true // 行为彻底由咱们控制, 这里无脑返回true
}
复制代码
改写以前的IsFileExist()函数:
func IsFileExist(fileName string) bool {
var mgr IFileNameManager = new(PureFileNameManager)
return mgr.IsFileExist(fileName)
}
复制代码
这样虽然添加了一个中间层, 可是仍是与外部依赖绑定了. 解决办法是: 使用依赖注入, 在被测试单元中注入一个伪实现. 依赖注入有两种方法: 构造函数注入和属性注入. 如何选择? 若是依赖项是必须的, 就用构造函数注入, 不然尽可能使用属性注入. 下面为咱们最终改造后的代码, 经过这样的改造, IsFileExist()函数就与文件系统的强依赖解耦了, 能够进行单元测试了.
var mgr IFileNameManager
func SetPureFileNameManager() {
mgr = new(PureFileNameManager)
}
func SetFakeFileNameManager() {
mgr = new(FakeFileManager)
}
func IsFileExist(fileName string) bool {
return mgr.IsFileExist(fileName)
}
复制代码
一个工做单元可能有三种最终结果: 返回值, 改变系统状态, 调用第三方对象. 对单元测试来说, 前两种结果与第三种有一个显著区别: 返回值和改变系统状态都是在当前系统中可观测到的, 能够直接对当前系统进行断言以验证测试结果正确性. 而调用第三方对象时, 当前系统的状态有可能未发生任何改变, 验证测试结果正确性须要对第三方对象进行断言. 这就引出了另外两个概念: 存根 (stub)和模拟(mock).
fake对象既能够做stub, 也能够做mock, 区别在于: 测试结果是否依赖对fake对象的断言 (也就是书中说的: stub不会致使测试失败, 而mock能够). 若是是, 那fake对象就是mock, 不然就是stub. 换言之, mock关注工做单元对外部依赖影响, stub关注外部依赖返回给工做单元的结果.
来看下面这段代码. 这一段实现了这样一个功能: 若是是星期六, 天气下雨, 就订一份外卖:
var kfc KFC // KFC餐厅
var address string // 个人收货地址
type KFC struct {}
func (t *KFC) OrderLunch(address string) {
fmt.Printf("Lunch is send out to %s, please wait!\n", address)
}
func DailyTask(date, weather string) {
addr := GetMyAddress()
if isSatuday(date) && isRainy(weather) {
kfc.OrderLunch(addr) // 给KFC发送一个订餐事件
}
}
复制代码
咱们如何肯定这个DailyTask()是否真的会在星期六的雨天发送这样一个订餐事件? OrderLunch()方法并无返回值, 也没有改变被测系统的状态, 须要检查KFC内部的状态. 所以, 咱们引入一个MockKFC对象, 调用该对象的OrderLunch()能够记录下传入的address参数.
var kfc IKFC // 把KFC变成一个接口, 便于注入mock对象
type IKFC interface {
OrderLunch(address) // 送餐
}
type MockKFC struct{
TestAddr string
}
func (t *MockKFC) OrderLunch(address string) {
t.TestAddr = address
fmt.Printf("Lunch is send out to %s, please wait!\n", address)
}
复制代码
这样改造以后, 测试代码就很好写了:
func TestDailyTask(t *testing.T) {
kfc = new(MockKFC)
address = "zju"
DailyTask("Saturday", "rainy")
if kfc.TestAddr != address {
t.Error("address not equal")
}
}
复制代码
通常来讲, 咱们不会手动管理mock对象, 而是采用mock框架帮助咱们完成这些事情. 书中第5,6章介绍了mock框架方方面面, 包括工做原理, 分类, 模式等等. 没太仔细看.
主要介绍了测试代码的组织方式 (第7章) 以及编写测试的最佳实践 (第8章).
首先须要明白的是, 对于一个应用程序而言, 单元测试和产品源代码同等重要. 测试代码一样须要维护.
优秀的测试应该同时具备以下三个属性: 可靠性, 可维护性, 可读性.
关于如何编写可靠的测试, 做者给出了几点建议:
单元测试命名很是重要. 一个测试名包含三个部分: 被测试方法名, 测试场景, 预期行为. 例如: Sum_ByDefault_ReturnsZero()
注意单元测试中的变量命名规范, 不要出现magic number, 应该在变量名中反映出变量的含义.
给出有意义的断言信息, 可以清晰地反映测试结果. 同时在代码上要将断言和操做分离, 不要把断言和操做写到一行里面.
不要滥用setup和teardown. 初始化模拟对象, 设置预期值这些操做应该放到测试方法中, 而不该该放到setup中. teardown通常用于集成测试, 在单元测试中, 只会在重置一个静态变量或单例的状态时才会使用.
这一章跳出了单元测试的技术细节, 转而从管理的角度探讨了如何推进单元测试在团队中落地.
如何在组织中引入单元测试? 做者给出的建议是: 小团队, 低风险项目, 领导者愿意接受变革. 须要注意, 必定要在确保你了解单元测试的基础上, 再推进单元测试, 万不可仓促实施单元测试. 此外, 还须要必定的政策上的支持.
做者指出, 要开始单元测试, 至少须要30%的工做时间. 然而, 引入单元测试并不必定会致使总体流程时间增长. 进行单元测试更容易在开发期修复bug, 从而减小集成测试的时间, 项目的交付期有可能提早.
单元测试是否会抢了QA饭碗? 不会的, 单元测试的存在会使QA更专一于寻找实际应用中的逻辑缺陷, 让QA专一于更大的问题. 有些公司QA工程师也写代码, 开发者和QA工程师均可以编写单元测试.
做者有一句话写得很是好: 你须要使用单元测试, 确保人们知道代码的功能是否受到破坏.
编码是代码生命周期的第一步. 在生命周期的大部分阶段, 代码都处于维护模式. "大部分的缺陷并非来自代码自身, 而是由人们之间的误解, 不断变化的需求以及缺乏应用领域知识形成的."
对一个遗留项目, 如何从0到1开始单元测试? 首先你须要列出项目组件的测试优先级. 能够经过逻辑复杂度, 依赖数, 重要程度判断其优先级. 通常来讲, 逻辑驱动的容易测试, 依赖驱动的难以测试 (须要mock).
在肯定了优先级以后, 须要选择测试策略, 先易后难, 先难后易, 各有优劣.
在重构代码前, 先进行集成测试, 确保重构时不会破坏原有功能.
什么是可测试的设计? 就是代码架构便于进行测试. 而测试的关键在于"接缝". 对静态语言来讲, 须要主动采用容许替换的设计 (即提供接口), 代码才能得到可测试性. 而对于动态语言, 可测试性设计就显得不那么有意义.
可测试的设计与SOLID原则相关, 通常来讲知足SOLID原则的设计都是可测试的设计, 而反之不成立, 所以: 可设计性并非优秀设计的目标, 而是优秀设计的副产品.
可测试设计会增长工做量, 编写更多的代码. 能够首先使用简单设计, 在须要时再进行重构.
书中提到了一些其余的技术书籍, 我的以为有些书籍还不错, 列举以下: