Eisen's Blog

© 2023. All rights reserved.

有关测试的读书笔记

December 06, 2016

tddunit-testatdd

刚刚培训结束,培训的不少时间是用来看各种乱七八糟的书籍。其中一个重要的领域就是有关测试的部分。这里整理一下前一阵子的笔记。

tdd by example

首先读了一本非常经典的书籍:tdd by example 也是测试驱动开发的由来了。不是第一次读这本书,但是这次读了之后依然有新的体会。读的时候就觉得书中的第一部分的小步都太小了,很多实现都是显而易见的。然而讽刺的是我自己在走这个例子的时候却因为步子太大而忘记在构造函数中设置 MoneyType 反而拖慢了进度。很小的步子隐含了按着步骤走可以大大减少中间出错而打乱节奏甚至是需要 debug 的风险,而debug 所花费的时间与小步走所花费的时间相比绝不是一个数量级的。在练习的过程中细细的去想为什么要这么写以及为什么是这么一个重构的流程引发了我找到了一些其他的书籍逐渐弄明白了书中很多忽略了的细节。比如第一部分中对 expression 钱包的隐喻其实并不是 tdd 所带来的好处,而是因为作者本身有这个设计思维,所以 tdd 不是所带来的益处。

另一个被很多人忽略的点在于每次在写一个测试之前是有一个 task 列表的。tdd 要求你每次写测试之前都知道自己想要实现的功能是什么,而让这个测试的通过就代表某一个小的功能的完成。列出系统想要什么样子并且可以有一个测试对应,这就是在做设计了,这就是 tdd 带来的主要好处吧~可是做不出来设计怎么办?那就是 ooad 的能力问题了。在有了测试之后就用最直接最简单的办法去实现,实现的方式有三种:

  1. fake 用假实现或者是空实现,等到必须要去实现的时候再实现
  2. obvious implementation 显而易见的实现,功能是实现了,不过采用的是最简单的办法,在未来可能会有性能问题,对其他的测试可能也不友好
  3. triangle 三角法,在不好实现的时候才使用的技巧,比如对于一个迭代实现,首先是单个元素的实现,然后逐步改为递归。在之前接触的一些 tdd 培训里面有些教练就认为应当用这种方法实现所有的 tdd 现在觉得有点过了。

实现之后 tdd 讲要做重构。这里的重构和 重构 那本书里面讲的很多是一致的。但是我之前在对重构的粒度和范围的理解确实不到位。通过各种形式的消除重复(代码重复、数据重复)把一个 fake 的实现逐渐转化为一个实际的方案才是重构的核心。

所以 tdd 是对重构的能力有很高的要求的。在之后的读书过程中逐渐的发现,重构是一个非常强大的技术,它不仅仅是在技术层面,在领域层面也是一个很好的工具。仔细想想一个复杂的,需要多年运行和维护的软件如何才能保证代码与架构不腐化堕落呢?唯有坚持重构了吧。

art of unit test

这本书其实主要讲的是单元测试而不是测试驱动开发。开篇它提到了 tdd 所需要的三种核心技能:

  1. 编写好的单元测试的能力,也就是这本书所讲的内容
  2. 通过测试驱动开发的能力,也就是 tdd by example 的内容
  3. 设计的能力,是很多其他设计书籍的内容,这里它推荐了一本书 Growing Object-Oriented Software, Guided by Tests

这本书还明确的定义了什么是单元测试:不是那种一个函数对应一个测试,而是对一个最小工作单元的测试。小巨人的培训里也是强调每个测试是完成了某个功能,这个测试就是这个功能的文档。它定义了一些好的单元测试的特性:

  1. 自动化
  2. 容易实现,几分钟
  3. 在一定时间内有意义,比如第二天,当然它强调的是单元测试而不是函数测试,不应随着重构而随意变化
  4. 可以一键运行
  5. 速度很快
  6. 结果稳定
  7. 完全隔离,独立于其他测试

它还强调了单元测试与集成测试的区别:集成测试结果不稳定、速度缓慢、多个真实依赖。比如依赖数据库、依赖文件系统。过多的依赖导致的是不能明确的指导什么导致了测试的失败。这也让我开始反思自己以前写的很多与数据库相关测试了。

还有它提出了优秀测试的支柱:

  • 只测试一个关注点
    • 测试多个方面会导致你的测试命名出现问题
    • 一旦第一个测试挂了,后面的断言就不再执行
  • 避免在测试中出现逻辑
  • 不测试内部方法:一个工作单元都是以一个公共 API 开始,最后达到三种结果中的一个或者多个,返回值、系统状态改变或者第三方调用
  • 不滥用 setup 方法,在保证测试的可读性与过度重构测试之间做权衡

Growing Object-Oriented Software, Guided by Test

这本书的思想正如标题所示,通过测试驱动建立面向对象的软件。它认为好的设计是可以通过测试一步步驱动出来的。但是它依然强调事先要知道一些基本的面向对象设计原则,这也是大家的共识吧。

书中提到了要将验收测试和单元测试结合起来,通过大循环套小循环来完成测试。

2016 12 06 review for test 04626

书中提到了通过 mock 和 stub 来隔离各个对象之间的依赖,这个其他几本书中也有所提及。

2016 12 06 review for test 131de

然后提到了一个对象出现的几个方式:

  1. breaking out: 将一个大对象拆分成几个相互合作的小对象
  2. budding off: 提出一个目前的对象所需要的服务,然后用一个对象去负责提供这个服务
  3. bundling up: 提供一个容器对象去隐藏相关对象的细节

在 TDD 的时候 breaking out 和 budding off 的动作会比较多。

测试驱动开发的艺术

这本书有点像是一个 cookbook。很多思想其他的书中都讲到了,我也没有留下非常深的印象。在有关 web 测试与数据库测试方面有一些值得记录的地方。

tdd for web

对于 mvc 来说 m 是不涉及任何 web 框架的,所以不用提。但是 c v 就是具体问题具体分析了。其中 c 的测试主要是依靠 request 与 response 的 mock。但是结合我目前前后端分离,后端都是 rest api 的情况来说更多的是采用类似于集成测试的方式。那什么叫类似呢?

  • 会启动一个临时的 server 从外面去访问这个 api
  • 但是所以来的一些东西可能是 fake 的,比如数据库、比如 session、比如外部系统

然后在测试 controller 的时候 view 就随着一起测试了。毕竟都是 json 而已。

tdd for database

首先说明了 db 的 tdd 用 unit test 是没有意义的,因为和数据库如此强依赖,把数据库 mock 掉感觉就没再测什么了。但是,如果每次测试都启动一个 mysql 也是听蛋疼的。所以这里人家采用了类似 fake 的方式:引入一个 memory database。这个是我应该学习的。之前都是直接连接 mysql 这也导致在 docker 里面启动 mysql 的情况。

在 teardown 做清理,采用 tx.rollback()

相关资料

  1. Test Driven Development
  2. 单元测试的艺术(第2版)
  3. Growing Object-Oriented Software, Guided by Tests
  4. 测试驱动开发的艺术