为什么极力支持测试驱动开发TDD

国内的一个很流行的博客上面讨论了TDD的一些问题,并且顺带批评了某咨询公司的咨询师不够脚踏实地。我在那个博客留言表达过我的不同意见,前几天随着另外一篇对TDD质疑的文章发表我和文章的作者在Twitter上论战了一番,不过论战是不能解决问题的(之前InfoQ上的虚拟讨论也没有解决问题),所以我再简单的整理一下我的意见。

首先,我声明我是一个笃信极限编程对『编程』有巨大价值的人。而TDD是极限编程XP里面的实践之一,在讨论XP的过程中我们一般都倾向于使用『实践(practice)』这个词,而不是『方法论(methodology)』,原因是极限编程描述的这些『方法』实际上是很多做事的具体方式,而不是一种『理论』。

我不应该咬文嚼字,不过有些时候这很重要。我需要从极限编程这个名字开始讨论这个问题。以前的一次OpenParty上,o6z问我『你知道极限编程为什么叫极限编程么?』,我当时真的不知道答案,只是隐约觉得极限编程的很多个体实践都是一种极限的挑战。而后o6z说『其实极限编程就是指程序员公认一些最佳实践,他们致力于不断的改进、优化这些最佳实践,最后把他们推向极致,这就叫极限编程』。这个答案可能是官方的,也可能是坊间流传的,不过我觉得它特别贴切,解释了极限编程的价值观。我当前公司的CEO上次调侃过『价值观』这个词,他说价值观说出来就不灵了,就成狗屁了……他说真正的价值观体现在你做每个决策的时候左右你的那种抽象的直觉。极限编程的每个写下来的『实践』其实就是写下来的价值观的体现,在实践的过程中最重要的就是不断的去磨练你的直觉,让价值观内在化。OK,极限编程我说道这里。

TDD,测试驱动开发。我们按照刚才阐述极限编程的涵义推演一下TDD是怎么来的。我们在写程序的时候发现不对程序进行细粒度的验证就很容易产生Bug,逐渐的整个编程社区有了一个最佳实践『单元测试』,我们也知道『单元测试』是相对于『集成测试』和『系统测试』的,我们提高程序内在质量的时候这些测试工具我们都要使用,只是有时其它测试会通过那种被叫做QA的程序员编写。为了不扯到另外一个话题,我们继续,当大家公认测试是最佳实践的时候,极限编程社区把测试推向极致。极致的测试应该具有细的粒度,高的覆盖率,有意义的验证条件,全面的边界条件,不脆弱等等,这些指标单个都不产生价值,但是在某个平衡的状态它具有最高的价值。极限的TDD的目的就是找到那个平衡。

我们退一步说,其实目前我们的程序员社区绝大部分人面临的都不是是否可以做好TDD,找到那个最佳的平衡点的问题。现在的主要问题还是是否可以写出有意义的测试,如何写测试的问题。其实质疑TDD的朋友经常的理由是『只要写好单元测试就可以了』,这其实正是我现在说的我们大多数程序员的困惑。也就是说『使用TDD』的对面是『能够写好单元测试但是不做TDD』,我认为这是一个伪命题。因为TDD的目的就是把测试这种最佳实践推向极限,这是一个过程,我可以把它分成两个阶段:

  • 第一个阶段是通过TDD强制从不写测试向写测试转变,因为绑定了写测试和写代码的节奏,它可以保证你写出的代码是可以测试的;
  • 第二阶段是通过不断实践和优化TDD让你能够写好测试。因为测试不只是有单元测试,还有系统测试和集成测试,随着对TDD的熟悉你会发现可以用不同层级的测试来驱动你的设计。使用越高层级的测试越有难度,这也是极限的一种体现。现在比较普遍的BDD其实就是将领域模型驱动DDD这种建模的方式与TDD结合的产物, Spec的描述形式让它不仅可以组织好单元测试,也可以组织好系统测试(如验收测试驱动设计,ATDD)。第二个阶段可以一直优化,永远没有极限,这个过程是最有价值的;

这种阶段化的实践其实内置了一些积极的意图:

  • 先写测试后写实现其实是把同时写测试和实现更近一步的产物。同时写测试的一种形容就是『可测试性驱动程序开发』,前几年老赵就写过这方面的博文,并且从可测试性上论证了TDD的积极意义。因为只有同时写测试和实现才能最好的保证你的代码是可测试的。而解决可测试性的难题的关键点是有可以检查验证条件(而不是足够细的粒度,细粒度既不是充分条件也不是必要条件,只是细粒度容易找到验证条件而已),也就是测试结果需要可验证。如果没有已知的可检查验证条件,那么可测试性就无法保证。所以先构思一个测试验证点再写代码是这个逻辑的体现。
  • 测试准备耗时费事,但是如果我们偷懒不做,那么后期可能面临的是完全无法做。保证测试容易准备需要在设计上多加考虑,如慎用Singleton等。这些思考可以影响你的设计,帮助你着力思考系统中的哪些状态是可变的,哪些是不变的,帮助你强化设计出无副作用或者少副作用的代码(因为有副作用的代码更难准备测试环境,环境的组合会多变),这帮助你函数化思考。
  • 红绿的节奏和小步前进可以帮助你减少对调试的依赖。调试和测试都是我们验证Bug的工具,不过我们最好在难以复现的场景使用调试这种终极武器,在编码阶段反复的进入调试说明你的测试有问题。编码阶段调试的常见原因就是测试没有跟上,因为我们知道最最常见的代码错误就是拼写错误。测试和调试找到拼写错误的代价是完全不同的。小步前进配合现代版本控制工具可以让我们完美的通过折半查找找到出问题的代码所在,如果有自动化测试套件的话折半查找还会事半功倍。这一条是说TDD的节奏所鼓励的小步前进的好处。
  • 测试驱动的测试需要有一个明确的名字。在寻找名字的过程你会重新思考这条验证的目的,让你整理需求的思路,也就是提醒你经常的问『为什么?』。细粒度的问为什么,并且为这些需求设计测试场景,这对每个想要贯彻『具体问题具体分析』的程序员非常重要。

这样的王婆卖瓜的理由我还有很多,多说无益。其实这里有一个关键的问题需要澄清,我们说TDD有这么多积极的意义,但是我们不能说『所有的代码都要TDD』,因为它很容易让TDD成为不现实的『生产力毒药』。当初比我经验丰富的一位同事就和我说『Spike(技术验证)的时候不需要TDD』,我发现在需要自由翱翔的时候放弃TDD的确是很舒服的事情,不过每当我们看这些Spike产生的代码时我们会发现TDD的重要性。因为这些 Spike出来的代码经常惨不忍睹。另外一位资深的同事就又和我说『Spike完成后,你应该删掉那些代码。然后重新TDD去实现它们,因为严格测试、精心设计过的代码才是为生产环境准备的』。我举的这个例子不能绝对化,不过我想表达的是,如果你想知道不做TDD的后果,那么一定要先做好TDD,回去对比观察没有TDD代码的不是。大部分关于TDD的批评大都来自那些还没有完全掌握TDD的人,工作流还没有很好的优化,此时对比『写单元测试』的自己就开始觉得TDD让自己混身不适了,『把最佳实践推向极限这个行为』要在完全掌握『最佳实践』的前提下才可以继续修炼。

我这里放一个比喻,我不知道是否贴切:有一种修行是爬看不到顶峰的山,视力可及的半山腰上有一片开满鲜花的平台。有些人爬到平台就下结论『爬到山顶也不过如此,也许还没有这么多鲜花呢』。但是另外一些人则继续攀登,以致山下都看不到他们的身影了。半山腰的人也许会开始质疑那些继续攀登的人的动机,说这完全是一种宗教。对于继续攀登的人来说,的确是一种信仰让他们坚持下去,那就是极限编程。关键的问题在于,对于那些山脚下的『沉默的大多数』人而言他们应该听谁的呢?是应该相信半山腰的人说『那些持续攀登的人走火入魔了,其实半山腰这里就最好了』?还是跟随那些持续攀登的人所走过的路走下去?其实,持续攀登的人会告诉所有山下的人你随时可以转身回到那个平台去,所以我们的行为是安全的,不过如果你持续攀登,那山上一定有一个更好的世界。有些人听了前者的话停留在山下徘徊,因为他们绝得那半山腰也不过如此,山下的日子很好过。但是有些人听了后者的话爬上了半山腰,还有一些也成为了后者。

这种比喻可以写的很华丽,不过这不是什么论据,它只是一种修辞而已。《思考的技术》这本书的第二章叫「逻辑打动人心」,我摘一些句子:

“但是”、“然而”这种话,对于改善经营而言,有白害而无一利

这是借口中常用的词,它会给出一个反面的评价,而后是中庸的『具体问题具体分析』。极限编程不是这样,它是单向的夸张,把最佳实践推向极限。所谓『写好单元测试就好了,要具体问题具体分析』其实是不写测试的一个好借口。如果你想让自己积极的去实践,请给自己一个极限的理由,当然,你随时都可以转身回到那个平台的。

如果给客户的药方,只是没有什么感觉的营养剂,客户的经营状况将无法改善

这是我想说明极限编程的极限的意义。这些推向极限的过程不是邪教,让你以为最后可以看到神迹而葬身途中,它的目的是给你一剂猛药,让你更好的走到那个平台,甚至到下一个层次上去(第一个平台的人兴许都不知道后面还有其它平台吧?)。

其实说到这里咨询公司的秘密也揭开了。咨询公司的医生不是包治百病的,他们的大部分都是希望给你一个最佳的『盗梦空间中的术语,植入想法,Inception』,期望这个植入可以帮助你向积极的方向前进,不过你的行为依然是你自己控制的。好的咨询公司不会利用这个机会『洗脑』,因为他们自己也在不断追求极致的过程中。这更像把酿造啤酒的技术推向极致的修士们(Trappist,非常著名的修道院啤酒的修士们通过几代人不断的优化酿造他们认为完美的啤酒,具有稳定而微妙的口味)所做的修行,他们自己在不断优化自己所做的事情,并通过咨询把这些Inception植入客户的思想。这里还要澄清一个问题,不是修道院中的所有修士都有崇高理想,有些新进来的修士不会酿酒,有些隐藏在修士中的南郭先生可能没有追求极致的精神,这非常正常,我们都是不完美的,我们的组织也不会完美,不过我们依然可以有追求完美的组织。

写到这里,我重复一下我在OpenParty的朋友中经常说的一句话『要把积极的影响施加给身边的朋友们』,我不像Cleverpig信仰巴哈依教,不过我坚持积极做人,积极影响人。极限编程和其中的TDD都出于同样的动机,施加给所有的程序员积极的愿望,不断的优化自己的工作流,以期达到最终的『极致』。谢谢观赏。

内文的一些链接我会稍后添加

后记:最近工作家里都忙,有了一个小公主需要伺候,所以没有太多时间更新Blog。我写博客不是让人围观的,而是写给自己和我所爱的家人朋友。我在Twitter上口水战还有写这篇博文主要是『质疑TDD和某咨询公司』与我的价值观冲突,所以我才不得不写这篇博文表达我的观点。我和我的朋友们还在努力组织好OpenParty的Unconference活动,我们的理想主义可以在这个活动上得到满足,我们给大家一个自由的分享与获取知识的机会,并且更重要的是我们要把积极的态度植入到参与活动的朋友的意识中,我想这是让我们生活更美好的最佳途径。

在VPS上装东西优先参考它们的wiki

有个朋友问我装OpenVPN哪个教程好,可那是很久以前搭的所以我不记得是用的哪篇教程了。还好,在Ubuntu的linode上面搭OpenVPN基本上没什么障碍。据我的经验,在VPS上面装任何软件最好的方式就是看注明VPS的wiki:

这两个都很不错,可以优先看看这里是否有需要的教程。

VPN还有其它选择,如PPTP和L2TP,它们在桌面和移动平台基本上都可以用,不像OpenVPN由于授权问题在一些封闭的移动系统里面不可以用。不过最好……你的VPS三种VPN都部署上,以备不时之需。

Leopard下如何让你的sudo不需要输密码

在Snow Leopard的Terminal里面用sudo的时候总是需要输入密码,而在单位的FreeBSD服务器上则不需要。我觉得从安全角度来说这个密码用处不大,有了反而会浪费时间。

搜索了一下,发现要这样做

修改/etc/sudoers文件(记得sudo vi它,存的时候要w!),去掉这一行前面的#号:

%wheel ALL=(ALL) NOPASSWD: ALL

然后执行下面命令,记得将your_username替换为你的用户名:

sudo dscl . append /Groups/wheel GroupMembership your_username

从此就不需要在sudo的时候输入密码了。

Cassandra的一致性级别

翻译Cassandra文档中的一段。

写一致性级别Write

ZERO
不保证一致。写操作在后台异步执行。除非CASSANDRA-685 修正,否则当太多写错作排队的时候,buffer会膨胀,后果很坏。
ANY
(需要0.6的Cassandra)保证至少写入到1个节点中,包括示意(hinted)的接收者。
ONE
保证写操作在回复客户端前至少已经进入1个节点的commit log和memory table。
QUORUM
保证写操作在回复客户端前至少被写入到 (<复制因数> / 2 + 1)个节点中。
ALL
保证写操作在回复客户端前已经被写入到 <复制因数> 个节点中。任何节点如果没有响应,则操作失败。

读一致性级别别Read

ZERO
不支持,因为没有意义。
ANY
不支持,应该使用ONE来代替。
ONE
将会返回第一个有响应结果的节点返回的数据。在使用ConsistencyLevel.ONE的时候,一致性检查永远是在后台线程中来修复一致性问题。这意味着如果开始的读取得到了旧数据时后续的调用可以返回正确的数据(这叫做读取修复 read repair)
QUORUM
查询所有的节点,一但绝大多数复制节点返回了结果,返回具有最新的时间戳的纪录。其余复制节点的结果会在后台进行检查。
ALL
查询所有的节点,一但所有复制节点返回了结果,返回具有最新的时间戳的纪录。任何节点如果没有响应,则操作失败。

python里面检测内存容量

今天要做一个python里面内存效率的测试,所以需要查看当前运行的python unittest的内存占用情况,查了一下发现psutil可以帮助你做这个事情,这样的代码就可以工作,需要安装psutil,我用的sudo pip install psutil:

class MemoryInfo(object):
    def __init__(self):
        self.current_process_id = os.getpid()
        self.current_process = Process(self.current_process_id)
    
    def usage(self):
        return self.current_process.get_memory_info()
    
    def print_usage(self):
        usage = self.usage()
        print '物理内存: %sKB, 虚拟内存: %sKB' % (usage[0]/1024, usage[1]/1024)

用的时候你可以实例化一个MemoryInfo()然后分多次调用它的print_usage方法。结绳记事。

Web as a platform

Matin fowler 大师在这篇文章 《Richardson Maturity Model: steps toward the glory of REST》,用一种伪成熟度模型的方式为我们揭示简单的Rest如何让Web成为未来软件开发的一个通用平台。其实我以前就职的ThoughtWorks里面的同事早就在研究Web as platform这个Buzz,因为这是一种通过成熟的标准让我们日常的开发更简单更容易被Mashup的好方法。其核心思想就是利用Rest和Atom让应用程序API变成Plain web应用,这样所有现有的Web应用api就很容易被你的应用Mash up进来,这样我们的世界就真的成为了一个网。所以,坚持Rest best practice,争取早点让你的系统建在Web平台上。

Atom在让Rest api连接起来,让你的应用变为以资源为中心很重要:

Python中的method_missing和*get*钩子方法族

刚才实验了一下,发现Python里面声明类的时候是否选择继承objects还是有很大区别的。只有继承了objects,才可以使用钩子方法如’__get__’, ‘__set__’, ‘__getattr__’, ‘__getattribute__’这些方法。也就是说这些有用的钩子方法是所谓的new object里面的东西。今天我在项目的代码里面尝试了一下类似Ruby method_missing的写法,实验在Python里面加点元编程的东西。发现很相似的三个方法’__get__’, ‘__getattr__’, ‘__getattribute__’方法区别挺大。注意,一定要继承object才可以享用三个钩子方法。

  • 访问对象方法的时候首先会访问’__getattribute__’,它是访问类里面所有属性的时候都要经过的方法,包括创建对象的时候访问’__init__’, ‘_meta’这些都回经过’__getattribute__’访问。如果你什么异常都不抛出,它就不会访问’__getattr__’方法。
  • 如果’__getattribute__’方法抛出’AttributeError’,那么会继续尝试访问’__getattr__’方法。再有异常抛出,那么这个类就没有钩子再接住异常了。所以从这个角度来说它的工作方式非常相似于Ruby的method_missing。
  • 我本以为’__getattr__’是系统内建函数hasattr(object, property)优先访问的方法。不过实验证明,实际上还是先走’__getattribute__’后走’__getattr__’的。也就是说hasattr这样的函数没有优先绑定’__getattr__’。
  • ‘__get__’方法是用来监视自己的类作为其它类的成员的时候被访问的钩子。对应的是’__set__’,是相应属性被赋值时的钩子。这个方法与’__getattr__’和’__getattribute__’完全不是一会儿事。刚才看《Python核心编程一书》完全没有解释清楚。《How-To Guide for Descriptors》这篇文章对解释’__get__’帮助很大,有兴趣可以看看,不过我倒是没有想到什么是合理的应用场合。

我目前还没有调用方法的method_missing,目前只是访问一些属性。我们实际处理的是一个可以直接用属性名读取/修改对象里面的持久化json属性的方法,就是类持有一个{‘property’: ‘value’}的json文本属性,我们就可以直接用Model.property访问和修改里面的方法,而不用特别的去生命json结构过来。是一个在Python中做meta programming的尝试。

测试刚才说的几个*get*方法的测试如下:

# -*- coding: utf8 -*-
import unittest

class A(object):
    def __init__(self):
        print 'init A'

    def __get__(self, *args):
        print '__get__ A', args

    def __set__(self, *args):
        print '__set__ A', args

class B(object):
    a = A()
    
    def __init__(self):
        print 'init B'

    def __getattr__(self, *args):
        print '__getattr__ B', args

    def __getattribute__(self, *args):
        print '__getattribute__ B', args
        raise AttributeError

    def __get__(self, *args):
        print '__get__ B', args

    def __set__(self, *args):
        print '__set__ B', args

class MeTest(unittest.TestCase):
    def test_simple(self):
        b = B()
        print b.a
        b.a = A()
        b.c
        hasattr(b, 'e')

在Mac下使用脚本重载proxy自动配置脚本(pac)

Mac下对网络设备使用proxy自动配置脚本可以透明使用代理穿墙(可以配合ssh tunnel和tor)。但是我一直不知道如何用脚本让系统重新载入pac文件(在更新了pac的规则时我们需要重载配置)。昨天一位叫做Dylan的网友留言告诉了我如何做,我在此记录一下。在命令行下面:

networksetup listallnetworkservices

然后会会返回一个网络连接服务的列表:


An asterisk (*) denotes that a network service is disabled.
Bluetooth DUN
ADSL
Ethernet
FireWire
AirPort
Bluetooth PAN

我一般需要配置pac文件的是Ethernet和AirPort,那么相应的重载命令是:


sudo networksetup -setautoproxystate 'AirPort' off
sudo networksetup -setautoproxyurl 'AirPort' 'file://localhost/Users/tin/pac/tin.pac'
sudo networksetup -setautoproxystate 'AirPort' on
sudo networksetup -setautoproxystate 'Ethernet' off
sudo networksetup -setautoproxyurl 'Ethernet' 'file://localhost/Users/tin/pac/tin.pac'
sudo networksetup -setautoproxystate 'Ethernet' on

然后pac文件就已经被重载完毕啦!在此感谢Dylan。

顺便共享一下我在bash下的alias:


alias px='ssh -qTfnNC -D 7777 tin@zztin.com'
alias rpx="sudo networksetup -setautoproxystate 'AirPort' off;sudo networksetup -setautoproxyurl 'AirPort' 'file://localhost/Users/tin/pac/tin .pac';sudo networksetup -setautoproxystate 'AirPort' on;sudo networksetup -setautoproxystate 'Ethernet' off;sudo networksetup -setautoproxyurl 'Ethernet' 'file://localhost/Users/tin/pac/tin.pac';sudo networksetup -setautoproxystate 'Ethernet' on"

DDD重构初步

所做的系统是一个连接到外部信息发送和搜索引擎调用服务的Web前端系统。系统与外部的接口使用的是一层Service外观进行包装,原先的设计目的是使用服务层剥离对外部系统的强依赖──解耦,同时还希望使用Service将系统的商业逻辑集中存放──提高复用的可能。但是实际上我们发现Service这样的抽取方法并没有提高代码的复用度,反而造成数据结构和其算法的大量重复。经过分析,发现由于使用了大量的Hash结构存放接口间的返回结果,造成数据与其Service内部的行为分离,传递后商业逻辑就出现了重复(这样的结果是引入大量Quick but dirty的解决方案所欠下的技术债造成的)。所以我和我的Pair绝对对其进行DDD(领域模型驱动设计)的重构。

领域模型驱动设计和面向对象设计在代码的抽象上很相似,它们都推荐让数据结构(状态)与商业逻辑(行为)统一管理。这样数据的状态与行为就不会分离,这样可以很大的减少由于商业逻辑分散造成的代码重复。这可以让我们实现我们非常重视的DRY(Don’t repeat yourself)。
一开始我们尝试的是找到重复的商业逻辑和其对应的数据结构,然后尝试让它们映射到我们的领域模型。这样的效果还不错,但是经过了一天的工作,我们发现我们对领域模型的理解有问题,我们居然抽取出了重复的领域模型类。这个时候暴露的问题是我们对系统的领域模型没有统一和深入的认识。

所以下一步我们计划进行领域模型语言的讨论会,目的就是使用自己的“领域语言”描述系统的所有行为,从这样的领域故事中找到我们的领域模型(领域概念模型,不是具体编程的时候的类)。

领域模型讨论会最好由最熟悉领域模型的人起草,可以由这个人把它写在白板上。领域的故事最好能够涵盖系统的主要行为,描述要使用简练的语言。一般来说IT系统的内部行为可能很复杂,但是到了领域高度还是可以用比较简单的语言描述的,如果遇到一块白板不够的话最好首先考虑提高观察的高度,让领域的描述简练一些,其次再考虑扩展到第二块白板。在这个过程中我们要注意消除歧义,比如同样一个“术语”在两个功能区域中出现,那我们就要妥善的给他们各自取一个容易区分的“领域术语”作为名字。还有就是我们应该在写领域故事的时候考虑领域模型(或者理解为系统模块的抽象)之间的交互关系,最好在写领域故事的时候对行为的归属(它会指导行为到底会被建模在哪个领域模型中,可以体现为类的调用关系,哪个类持有交互逻辑)达成共识,这个时候达成的共识比在做具体的OO设计时候分辨商业逻辑归属要更体现商业价值。完成领域故事后我们要找到其中的所有领域模型(也就是前面说的领域对象和领域中的术语)。然后团队最好一起通读这个用户故事,一起讨论是否通顺(行为是否完备,抽象是否合理),是否有遗漏(遗失的领域模型或行为)。对于有外部系统的情况下,即使外部系统是面向消息的(或者说是没有使用领域模型驱动的SOA接口),那么团队最好对消息的内容有一个讨论,对其中设计到本系统和外不系统的领域模型进行认领,然后使用一个适配器来保证数据到达系统后就使用领域模型表示,外不系统的数据(一般没有行为)最好也使用一个领域模型进行约定。

完成了上面这一步后,DDD最重要的一部分就完成了,它可以实现系统领域模型自顶向下的“名正言顺”,减少在自底向上的重构过程中产生的大量重复领域模型。下面的重构过程就是给领域模型写测试,使用TDD(或者也可以用BDD的方式提早对行为做验收测试,用它们来驱动对领域模型的实现,其实对领域模型的TDD和BDD是殊路同归)的方式逐一实现领域模型。而后争取给系统写一些验收测试或者高级别的集成测试,再逐一替换这些领域模型。这样就达到了DDD重构的效果。

可测试性驱动开发还不是“测试驱动开发”

看完老赵精彩的两篇“可测试性驱动开发(上)”“可测试性驱动开发(下)”

写了篇回复

老赵,我觉得你说的“可测试性驱动开发”和“测试驱动开发”的结果是非常接近的,可是实际观察下来我觉得不是一样的。我觉得测试驱动开发的一个好处就是从外界向内部逐渐迫近,逐渐的暴露的你意图。意图其实是更接近你的商业流程和商业价值的。所以更接近与“意图驱动开发”。因为TDD鼓励从系统的外部行为向内部行为逐步细化,逐步测试-通过,循环。这样产生的结果就是方法小,方法名考究,高层的代码非常容易读懂(经常达到看起来就像文档)。

可是在“可测试性驱动开发”中,我觉得随时考虑的还是“可测试性”,但是“可测试性”本身与商业价值无关。所以驱动开发的东西就转变为程序员自己的一些“修养”问题了,你可能会倾向于使用模式来解决一些微观问题,让代码具有“可测试性”。这种做法可以产生组织良好的代码,它对于程序员来说易读(容易理解算法细节),但是这样的代码在被维护更久以后容易产生与商业价值的偏离。还有就是“测试性驱动”的经济问题,如果一个测试是纯技术意图驱动的,那么就是追本逐末了。

测试驱动开发中的ATDD(验收测试驱动开发)鼓励你将系统的流程Spec在实现前写出来作为驱动,而BDD则从微观行为上将你的领域模型的行为Spec写出来驱动,这两种方式我想才是经典TDD的“新外衣”,因为他们都继承了“意图驱动”并进化为“商业意图(价值)驱动”。

此文甚好,只是我觉得上面问题是个关键,”目标“最好还是在产生”策略“之前找到比较好,所以才写这个评论,冒犯了 ^___^