# TDD中的驱动
TDD中的测试针对的粒度是独立的功能上下文或者变化点,测试验证功能上下文或变化点是否符合功能需求。对于相同的功能,如果我们划分的功能上下文不同,最终的实现方式也会不一样。不同的实现策略,隐含着不同的功能上下文划分,针对不同的功能上下文,我们编写对应单元级别的功能测试来验证其功能。在这些单元级别的功能测试的指引下,就可以逐步完成软件的功能。也就是说,功能上下文的划分,指引我们编写测试,在测试的驱动下,我们逐步完成功能上下文的实现。
从这里我们就可以看出测试驱动开发的核心要点是:单元级别的功能测试能够驱动其对应单元的外在功能需求。而对应单元之内的实现,测试就没有办法了。比如Args.parse()
这个方法,我们分解的时候是内部的实现方式,但是从功能测试的角度来看,却是不知道内部是如何处理参数列表的。那如果我们要驱动单元内的功能实现,该怎么办呢?
这时候可能就要将这个单元对应的功能上下文分解为更小的上下文,并将功能需求在这个上下文中加以分解。例如Args.parse()
是一个大的上下文,按照我们的实现思路可以将它分解成一个小的功能上下文,将参数列表分解为映射。然后再将参数列表分解放入另一个单元,对它进行测试,从而驱动它的实现。也就是说,单元级别功能测试无法驱动小于其测试单元的功能需求,也无法驱动单元内部的实现方式,需要进一步拆分上下文。
对于TDD来说,如果一定要指明某个单元内的实现细节,那这种行为是毫无驱动力的。比如使用冒泡算法对数组进行排序,从功能的角度来说,冒泡和其他的排序算法是没有差别的,那如果要在测试中体现不同算法的差异,驱动不同的实现就会产生非常反常规的代码。所以,测试驱动开发的主要关注点在单元间的分配,而不是模块内如何实现,这就需要开发人员有自己的想法。
如果开发人员并不知道如何实现,那么利用TDD的mock
,一样可以模拟出我们对于这个单元的期待,希望有什么样的输入,最终产生怎样的输出。现在chatGPT甚至可以基于测试的期待,帮忙生成实现代码,所以TDD的红、绿、重构就是一个从无到有,从凌乱到简洁的这么一个架构演进过程。
# 驱动中的重构
从驱动的角度来讲,TDD并不是一种编码技术,它无法驱动我们实现认知范围以外的代码,但是TDD能够通过测试与重构,驱动单元的划分以及功能的归属。在TDD中,重构是和测试一样的驱动力,驱使我们得到更好的架构和更清晰的代码结构。
提取方法和(Extract Method
)内联方法(Inline Method
)是TDD中两种最重要的方法。他们相当于是有语义化的查找和替换,在不破坏代码结构的前提下,完成查找和替换。这种手法通常是将需要修改的代码提取到新方法中,在新方法内完成要做的修改,再通过内联的方式在所有调用这个新方法的地方完成修改,这两种手法是修改代码的基本方式。
在提取方法的基础上,我们可以进一步将提取出的行为从当前的对象中分离出去,也就是提取对象,一旦提取出对象,我们就能通过引入类内字段(Field)、参数(Parameter)等方式,不再直接引用当前对象上下文,从而将其与当前对象上下文分离。可以使用的重构手法有引入字段(Introduce Field)、引入参数(Introduce Parameter)等。通过这些手法我们可以对类的结构进行调整,也就是对模块的重新划分以及重新分配。当然,也可以通过相似的手法完成的单元合并。这个过程中,整体的原则就是:对修改封闭,对扩展开放。这种架构改进方法叫做重构到模式,即:将架构上的坏味道替换为设计模式。这是一种有效的架构软件方法,用公认的好设计模式替换公认的不好的设计。
这种从功能测试出发,逐步完成软件开发,但是架构不是预先设计,而是在完成功能的前提下演进出来的模式,被称之为演进式设计。通过重构到模式演进的获得架构,是一种实效主义编码架构风格。
# 两种流派
红、绿,重构循环中的重构,是在完成功能的前提下以演进的方式进行设计的,这是一种延迟性策略,也叫最晚尽责时刻。这种策略的重点在于,保持决策有效性的前提下,要尽可能的推测决策时间。如果架构不清晰,那么其实我们就不必花费时间进行空对空的讨论,可以尽早开始实现功能,再通过重构从可工作的软件中提取架构。这种方式被称为TDD的经典学派,也叫芝加哥学派。
除了经典学派,还有另外一种风格,被称为伦敦学派,如果架构愿景比较清晰了,就可以使用伦敦学派进行TDD。伦敦学派的做法是这样的:
- 按照功能需求与架构愿景划分对象的角色和职责。
- 根据角色与职责,明确对象之间的交互。
- 按照调用栈的顺序,自外向内依次实现不同的对象。
- 在实现的过程中,依照交互关系,使用mock替换所有被实现对象直接关联的对象。
- 依次将所有对象实现完成。
经典学派强调功能优先,设计和架构后置,通过重构进行演进式设计,伦敦学派并不排斥预先存在的设计,而是强调如何通过mock将注意力集中到功能上下文中的某个对象上,然后再测试的驱动下,按部就班的完成功能开发。他们都不是将功能整体作为单元的粒度,而是选择了更小的范围。我们可以将轮到学派看作一种利用架构愿景分隔功能上下文,然后再进入经典模式的TDD方法。这么做的好处是,对于复杂的场景,可以极大简化构造测试的时间。在功能上下文内,主要以经典学派为主,在跨功能上下文时,可以使用伦敦学派对不同功能上下文进行隔离。从驱动的角度来说,TDD实际上并不是一种编码技术,而更像是一种架构演进技术,它可以帮助我们更好的将功能放置到不同的单元。