10 TDD中的驱动(3):何为经典学派?何为伦敦学派?
你好,我是徐昊。今天我们继续来讨论测试驱动开发中的驱动。
上节课我们讲了四种常用的重构手法,分别是提取方法、内联方法、引入参数和引入字段。并区分了用于高效修改代码的重构手法,和用于消除坏味道以改进架构设计的重构。还介绍了一种架构软件的方法:重构到模式。也就是通过重构将坏味道替换为模式,从而改进软件架构的方式。
重构到模式,或者说TDD 红/绿/重构循环中的重构,是在完成功能的前提下以演进的方式进行设计。这是延迟性决策策略,也叫最晚尽责时刻(Last Responsible Moment,LRM)。也就是说,与其在信息不足的情况下做决定,不如延迟到信息更多,或是不得不做出决策的时机再决策。这种策略的重点在于,在保持决策有效性的前提下,尽可能地推迟决策时间。
如果架构愿景不清晰,那么“最晚尽责时刻”让我们不必花费时间进行空对空的讨论,可以尽早开始实现功能,再通过重构从可工作的软件(Working Software)中提取架构。这种方式也被称作TDD的经典学派(Classic School)或芝加哥学派(Chicago School)。
除了经典学派之外,还有一种TDD风格,被称作TDD的伦敦学派(London School)。如果架构愿景已经比较清晰了,那么我们就可以使用伦敦学派进行TDD。
命令行解析
伦敦学派的做法是这样的:
- 按照功能需求与架构愿景划分对象的角色和职责;
- 根据角色与职责,明确对象之间的交互;
- 按照调用栈(Call Stack)的顺序,自外向内依次实现不同的对象;
- 在实现的过程中,依照交互关系,使用测试替身替换所有与被实现对象直接关联的对象;
- 直到所有对象全部都实现完成。
接下来我仍然以命令行参数解析为例,演示一下如何通过伦敦学派的方式来开发它。
首先是明确我们的架构愿景,也就是对象的角色与职责划分。如下图所示:
如上图所示,在系统中一共存在四个类:作为对外API的Args,从参数列表中提取参数的ValueRetriever,将Java类对象封装为选项的OptionClass,以及根据类型和数据解析参数的OptionParser。它们的交互,也如上图所示。
在划定角色和职责之后,我们来依次实现它们。视频演示如下:
与经典模式的差异
通过上面的视频展示,你能明显体会到“伦敦学派”与“经典学派”的差异:
- 经典学派强调功能优先,设计/架构后置,通过重构进行演进式设计。
- 而“伦敦学派”并不排斥预先存在的设计,更强调如何通过测试替身,将注意力集中到功能上下文中的某个对象上。然后在测试的驱动下,按部就班地完成功能开发。
然而二者其实并没有什么冲突。回顾一下第八讲的演示,以及本节课介绍伦敦学派时的演示,你会发现二者有相似之处:都不是将功能整体作为单元的粒度,而是选择了更小的范围。所不同的仅仅在于,在本节课中,我们更为细致地划分了功能上下文(也就是做了更多的设计)。
所以我们可以将伦敦学派看作一种利用架构愿景分割功能上下文,然后再进入经典模式的TDD方法。这么做的好处是,对于复杂的场景,可以极大简化构造测试的时间。下面让我们看一个例子:
在这个例子里,我们可以通过重构去获得想要的架构,比如提取出Repository等。但是,以经典学派构造测试过于麻烦,而如果以伦敦学派开始就可以简单很多:
需要提醒一下,经典学派和伦敦学派是TDD中都需要掌握的基本功。在功能上下文内,以经典学派为主;而跨功能上下文时,可以使用伦敦学派对不同的功能上下文进行隔离。
顺便说一句,曾经有一段时间,TDD社区内将这两种风格对立起来:
- 经典学派质疑伦敦学派向预先设计妥协,放弃了演进式架构。
- 伦敦学派攻击经典学派不愿吸收借鉴行业积累,“难道每次都要从一个大类重构到MVC吗?”
然而这种分歧是无意义的。在不同的场合下,我们只需要使用该用的方式就好。
小结
通过第8-10这三节课,我们讨论了测试驱动中的驱动。也回答了测试驱动到底能够驱动什么:单元级别功能测试能够驱动其对应单元(功能上下文或变化点)的功能需求。而对于单元之内某个功能的实现,则无能为力。
从“驱动”的角度来说,TDD实际上并不是一种编码技术,更像是一种架构技术,它可以帮助你更好地将功能放置到不同的单元。于是我们继续介绍了TDD中两种处理架构的思路:延迟性决策的经典学派和通过架构愿景划分功能上下文的伦敦学派。
至此,我们已经对测试驱动开发有了较深入的了解,知道了什么样的测试,提供什么样的驱动。那么下节课,让我们看看测试驱动开发的全貌。
思考题
在伦敦学派中,是如何保证测试的有效性的?
编辑来信:
TDD是一项技能,唯有动手实操、反复练习,才能有所小成。为了帮助你更快地进步,徐昊老师特发起了“代码评点”活动。
在第一个实战项目结束后,我们会根据你提交的学习反馈,手动选出其中几位进行代码评点与解疑答惑。而评点的详细内容我们也将制成加餐,展示在专栏里,供其他同学学习与参考。
划重点!如果学完第1-10讲再写反馈,将会大大提高你入选的机会!另,此次收集时间截至4月3日零点。所以非常希望你能跟上我们的更新进度,多动手实操,并记录学习体会。
最后,希望我们都能好好学习,更上层楼!
- 汗香 👍(4) 💬(1)
我认为保证测试有效性有两点: 1、不能对待测方法的实现使用测试替身,可以对测试方法调用的外部组件使用测试替身 2、不能行为验证绑定到实现方法的内部 另外我有个问题,最后一个视频演示的是 Controller -> Dao 的场景,这种直接对 Dao 使用测试替身,这种方法毋庸置疑,但如果是多层构架,如 Controller -> Service -> Dao ,那么我们在测 Controller 时应该是对 Dao 使用测试替身还对 Service 使用测试替身
2022-04-17 - 枫中的刀剑 👍(0) 💬(1)
对于伦敦学派,根据第5、6章节的内容,也是主要是通过状态校验来保证测试的有效性。但是有个疑惑点是对于伦敦学派,采用状态验证的方式只能保证单个sut的有效性,而对于更大的功能上下文的有效性是应该没法保证的,所以对于完整功能的有效性验证就只能通过更大的端到端测试来保证吗?
2022-03-30 - 姑射仙人 👍(2) 💬(0)
在伦敦学派中,是如何保证测试的有效性的? 状态验证
2022-05-21 - aoe 👍(2) 💬(0)
测试入参:伦敦学派中通过“Mock 外层”返回了当前需要的数据,测试时隔离了对外层的依赖,降低了构造测试的难度。 功能验证:使用状态验证 伦敦学派通过“Mock 外层”返回了当前需要的数据,本层功能依然使用状态验证,所以可以保证测试的有效性。 08 09 10 学习笔记 http://wyyl1.com/post/19/06/
2022-03-31 - Geek_b11f27 👍(0) 💬(0)
我感觉伦敦学派就是我原来印象中的单元测试,仅仅测试单个的方法的内部逻辑是否符合预期,其它所有依赖的外部元素都是Mock,伦敦学派保证测试的有效性的一个关键点就是千万不要mock被测试的方法,否则就是自己骗自己玩。
2024-07-01 - Geek_32ff72 👍(0) 💬(0)
最后的例子把repo mock了,但是controller->repo的正确性怎么保证呢?是否还需要一个完整的集成测试呢?
2023-01-16 - 王艺辉 👍(0) 💬(0)
伦敦学派初始将自己视作更大的功能上下文通过替身保证测试有效性;将注意力集中到功能上下文中的某个对象上之后,依然由芝加哥学派来保证测试有效性。
2022-12-03 - SochiLee 👍(0) 💬(1)
古典学派到底是叫芝加哥学派还是底特律学派呀?《Mocks Aren't Stubs》中说古典学派叫底特律学派。
2022-06-21 - zenk 👍(0) 💬(0)
在伦敦学派中,是如何保证测试的有效性的? 架构愿景清晰,保证可以构造稳定的测试替身,进而保证测试的有效性。
2022-05-07 - davix 👍(0) 💬(0)
在伦敦学派中,是如何保证测试的有效性的? 我覺得是通過更高層的集成測試保證的。比如repository的測試要用真實數據庫
2022-05-03 - wenming 👍(0) 💬(0)
思考题我是这么理解的:只要构造 Stub 的时候不对 SUT 的结果产生影响,也就是说,Stub 的存在不会改变测试的效果,只是我们构建夹具(测试上下文)的形式或者说方法有区别,那么整个测试的有效性就能够得到保证
2022-04-01 - 邓志国 👍(0) 💬(0)
保证测试有效,就得用足够好的测试替身。
2022-03-31