TDD死了 测试永存

这是Rails之父David H. Hansson (DHH) 一篇否定单元测试TDD引起广泛争议的博文,大意翻译如下:

测试优先的原教旨主义就像是禁欲教育:是一个不切实际的,无效的道德活动,让人自我厌恶和羞耻。

它一开始并不是那样。当我第一次发现TDD,它像一个有礼貌的邀请,一个更好的编写软件的世界。一种心灵萌动驱使你去实践测试。一个测试代码库好得那么宁静真是让我开眼,它给予了软件变革的信心。

测试优先是一个精彩的自我训练轮,教我如何在更深的层次上对测试进行思考,但我很快离开就离开它。

多年来,优先测试的言论越来越让人生气。有时我也被卷入这种原教旨主义漩涡,因为没有跟上这种福音让我自我感觉很不好。我会尽量试验测试优先数周时间,当它开始伤害我的设计时我抛弃了它。

..

不说更多了,我的名字是戴维,我是不以测试优先为原则编写软件的。而且我拒绝道歉,更不隐藏它。我很感激TDD并打开我的视野看到自动回归测试,但我早已经离开设计教条。

...

第一步是承认有问题。我想我们已经采取了。第二步是从单元到系统重新平衡测试。目前的狂热的TDD经验导致聚焦单元测试,因为这些测试能够驱动代码设计(测试第一个原始的理由)。

我认为这是不健康的。测试第一的单元测试会导致过于复杂的中间对象,它会诞生一些架构上可怕的怪物。比如茂密丛林b般的服务对象和命令模式,还有更糟的。

我很少进行单元测试,我直接测试Rails的Active record,让它们直接访问数据库,然后在其上部防止一些控制器的测试,但我宁愿使用Capybara 更高水平的系统测试取代它们。

我认为这是我们前进的方向。不用太重视单元测试,因为我们不再做测试优先的设计实践了。更强调可能是缓慢的系统测试。(顺便说一句,不需要惧怕慢,感谢云计算或并行化计算的发展)

Rails帮助我实现了这个转变。今天我们不用如何鼓动全系统测试,因为一旦有错误就必须解决,但是你可以不必等到其发生,但你不必等到发生的。让Capybara今天运行,你会有一个美好的明天。

测试优先对于我来说是死了,系统测试才是我的金色教堂。

[该贴被banq于2014-04-29 07:37修改过]

大师Robert Martin Bob大叔针对ROR之父提出了反对意见:

"测试优先的原教旨主义就像是禁欲教育:是一个不切实际的,无效的道德活动,让人自我厌恶和羞耻。"

原文中两个单词:“测试优先原教旨主义”。原教旨主义是用来表示:“回归基本”;但由于911已经采取了暴力极端主义的内涵(原教旨主义导致极端暴力)。Bob大叔说:我还没有看到因为人们反复叫喊:“Kent Beck万岁”而导致任何测试驱动开发的飞行飞机撞击大厦,所以我必须完全否定其内涵。

那么测试驱动开发者会热爱自己的专业吗?当然,包括我自己在内;但我还没有听到关于TDDer们会通过武力去征服别人。所以,我必须得出结论,作者用了“原教旨主义”形容我们这些谁搞测试驱动开发的存在一个深深的贬义与嘲笑。

“禁欲性教育”。据我所知,没有人真的教禁欲性教育。而是教导一夫一妻制:一种坚定的关系,在一个确定的上下文范围内如婚姻内保持性接触。

不过在某些宗教团体一夫一妻制会紧密到道德倒是真的。不过,也有思想世俗的学校(我自己),简单地认为一夫一妻制是不会导致深刻的道德后果的良好的个人选择。鉴于HIV流行开始于上世纪80年代和90年代,或70年代的疱疹疫情 ;并考虑到单身母亲与贫困有很强的相关性,也许一夫一妻制不应该被用来作为一个贬义的形容词。

当然,人们可能会认为,鉴于50%的离婚率,一夫一妻制的策略也许不理想。然而,如果婚姻能够持续到丧偶,而二次婚姻也是如此,这样的一夫一妻制似乎显得好一点。

而且,毕竟,从一个纯粹的实用角度出发,每个人根本就没有更好的方法能够预防疾病和不必要的孩子。 (而且,至少在我的经验,一个快乐而有意义的生活。)

"是一个不切实际的,无效的道德活动,让人自我厌恶和羞耻"则显得好笑。

我明白作者想说什么。在TDD中有一些让他不舒服的刺耳说教。我自己已经使用了说教;我相信说教是必须的。原因很简单。在这个混蛋行业,如果你不这样实施TDD,或者类似TDD某种有效的措施,你会觉得非常糟糕。

我们为什么需要TDD?我们做TDD的一个重要的原因和几个不太重要的原因。不太重要的原因是:
1.我们花更少的时间调试。
2.测试作为在系统的最底层准确,精确,毫不含糊的文档。
3.编写测试首先需要去耦其他测试策略不需要的;我们认为,这种脱钩松耦合方式是有益的。

这些都是TDD的附带效益;他们是值得商榷的。然而,一个重要好处是:

如果你有一个你信任的测试套件,你愿意部署那些完全通过测试的系统; 如果该测试套件可以在几秒钟或几分钟来执行,那么你就可以快速,轻松地没有恐惧地清理代码。

如果开发人员可以快速,轻松地清理代码,而不必担心破坏任何东西,那就是好处。因为如果你可以有一套干净代码,你就可以保持开发团队避免陷入泥坑。你可以保持团队快速移动。

使用Rails的一些人主张使用数据库和GUI(使用 Capybara)进行测试。如果满足我提出的可信性和速度两个目标,那么尽管可以去做,如果你相信那些多集成测试能执行得很快,当他们通过时你愿意去部署;而且可以连续有效地重构和清理的代码,那么就去做吧。

但是,这在我看来,集成测试很少满足我的两个目标。首先,我怀疑他们能否获得必要的可信度,因为他们通过GUI操作;你不能从GUI访问所有的代码。在一个正常系统中有大量的代码是例外,它们在错误或奇怪的角落,这是无法通过正常的用户界面来达到测试母的。事实上,我认为你测试时只能覆盖多一点,超过一半的代码,但这似乎不太可能,我认为任何人会愿意基于测试部署,但是如果发现有这么一大块代码没有经过测试肯定是不愿意的。

其次,这种快速测试对我来说似乎不太可能,尽管在云中运行着数百台服务器,你可以得到那些需要有效和持续重构代码的速度,但是数据库和图形用户界面会很慢。

我想可能是错的。我很乐意被证明是错误的....

支持DHH又一篇文章,认为测试会导致不必要的复杂性,间接性和抽象,一个坏的设计就算是有一套完整的测试也是坏的设计。
Test-induced design damage

“代码很难单独测试是因为设计不当”,这是TDD格言。隔离意味着与上下文和协作者切断依赖,特别是一些“慢”的东西,如数据库或文件IO。“单元”的普遍定义是单元测试(虽然不是每个人都同意这一点)。

这有时是对的,但是有时很难对坏的设计设置切入点,比如乱糟糟的职责行为或其他什么,这就说明难于单元测试并非一定总是因为设计不够充分,其实,你精心设计的代码,却很难进行单元测试。

这种不幸是因为测试导入的设计影响了流程,所谓测试导入的设计是指当改变你的代码,有如下考虑:要么有利于a)更容易测试先行, b)快速测试,或c )单元测试,但是这样做会损害代码的清晰度。引入了不必要的间接概念。设计被扭曲的变形只是为了适应测试目标。

忠实TDD'ers拒绝这种情况发生的可能。一个真正的信徒是只从好的方面来看待测试。在云计算的审判下, TDD已经从专业编程的救星的位置上跌落到被审判的地步。当前 “ TDD已死”的起义孕育出生了。


我想为什么我们已经能够这么长时间没有去质疑TDD到底是否值得作为设计原则的一部分,其实这是事后合理化而已。如果您接受的前提是红 - 绿 - 重构(red-green-refactor)是所有编程设计的真正的指路明灯,在它的祭坛上的任何牺牲似乎微不足道。谁在乎你需要间接的两个或三个额外的层进行单元测试控制器?当然,这是值得的。


六边形(Hexagonal)设计的损坏
这方面的一个很好的例子是已故的Jim Weirich在Rails的示范六角形结构(https://www.youtube.com/watch?v=tg5RFeSfBM4)。本演示展示了“从Rails解耦”的做法,。
当你观看演示,听取设计理据。他们都是有关测试!这是关于拥有更快的测试,而不触及数据库,它是关于能够测试控制器逻辑却不依赖上下文。

为了实现这一目标,简单的控制器直接禁止访问Active Record的,得通过仓储资源库(Repository)。而Action本身是被挖空的,只是提取一个Command对象,这个Command通过监听模式再回调控制器。

这并不好

该代码为实现两个测试目标已经遭受了巨大的设计损害,测试目标是:更快的测试和易于模拟(easy-to-mock)的单元测试控制器。这是试图在在“应用程序”和Rails之间进行分界,支持者喜欢分开它们两者。

六角形(hexagonal)设计模式本身并不为怪。有可能是一种正确的方向,特别是对于将Web应用领域之外的地方。也许,如果你正在做一个带有常规GUI界面或语音控制或什么的系统,而这些不能只通过一个Web API , Web界面只是众多界面中一个而已。

但是,六边形图案为了实现测试驱动模式的应用被误用为Rails应用中了。

同样使用其他模式一起来实现这一目标。将对ActiveRecord访问隐藏在仓储Repository后面,作为一种与数据库解耦的方式从而实现快速测试目标,它并没有阐明应用程序本身的设计。如果你不关心测试,你就不会看“后面的代码” (访问ActiveRecord的代码) ,并说“好! ” 。

请远离“单元测试是最好的”口头禅,得出这样的一个结论是因为,我认为在Rails(或类似的MVC设置)实现单元测试控制器是一个错误的尝试。控制器的作用是将在一个上下文中对模型的请求与响应结合起来。(banq 注:这是MVC模式对控制器的定义,控制器不能被挪作单元测试用,改变其当初用途)。

控制器目的是要集成测试,而不是单元测试。但是测试金字塔规定,单元级别的测试,其重点是,让人们在默认不知晓的情况下吃进。

我认为对视图进行单元测试是同样愚蠢的,这会将更多关注和兴趣集中在视图,因为视图是MVC这个蛋糕的顶层表面,所以对其相应的测试被比喻为系统测试:端对端。


最后,恐惧模型直接数据库对话的想法已经过时。这种解耦简单的根本不值得一提,现在的Rails作为一个持久存储的控制台程序运行,所以花费1.2秒的开销来引导框架运行单元测试的例子已经一去不复返了。


也许BDD的支持者可能会振振有辞地说,是的,单元测试并不是我们应该做的。我们应该从外而内。但只要这也是在测试这一制度下实施,我认为它通常还是于事无补。它仍然会导致过度Mock和和人工划分边界。

最重要的是,不要让你的测试驱动你的设计,而是让你的设计驱动你的测试!要按照MVC模式中规定的正确分层指导你的设计。

个人还是觉得, 要真的实现低耦合,从 一开始就tdd是非常必要的。因为那样会让你提前思考如何设计。当然, 从投资回报的角度,有些项目可以不那么追求质量。

大师阵营的Martin fowler专门写了一篇单元测试文章:
http://martinfowler.com/bliki/UnitTest.html

对单元测试进行了定义,其单元的意思是以类Class为单位,然后从隔离和速度两个方面谈了自己的想法,在持续提交中,单元测试应该包含在持续部署中,至少每天运行一次。

持续交付的的概念和工具介绍:
http://www.jdon.com/project/continuous-delivery-tools.html

Martin Fowler在其 “单元测试”一文中对单元定义是一个类,一个类中可能有很多方法行为,单元不能粒度太细,也不能太粗,太细了就容易范DHH批评的宗教原教旨主义,也就是形式主义,影响了正常软件设计意图,太粗了则达不到持续交付的目标,软件BUG频出。

这场讨论是来自Rails之父DHH提出“TDD已死”的讨论,主要是针对单元测试在Web框架比较难以实施,因为普通Web框架中就是一个MVC框架,Model实际是数据库,在MVC结构下单元测试无法插足,一般是使用变种的Controller,这使得C控制器的含义变成两个,一个是MVC的控制器,用于数据库模型Model和视图View的交互;另外一个专门用于对模型Model测试的,”TDD已死“派认为这样不妥,破坏了控制器原本设计含义。

其实,banq我个人意见还是坚持以前的想法,这种尴尬是因为MVC这种不合时宜的结构模式造成的,我2010年曾经写了一篇:MVC模式已死,里面实际谈到了MVC模式的僵化,建议以DCI等架构,当然包括后来的DDD/CQRS来替代,从今天观点来看,这样才能真正避免MVC模式和单元测试的冲突。

Ruby阵营的另外一位牛人Andrzej Krzywda发表了
TDD and Rails - what makes a good Unit?更加佐证了我当初的观点。

在这篇文章中,AK认为单元的定义需要从你采取的技术架构上定义,在你应该知道的四种架构一文中已经谈及了六边形Hexagonal架构 干净的架构和DDD/CQRS以及DCI。

AK举了一个例子,如果你有订单Order, 一个订单中有很多订单条目OrderLines, 以及一个货运地址ShippingAddress和一个客户Customer. 那么你是否为这四个对象建立四个单元测试呢?很显然不是,而是只要为Order这个类建立一个单元测试就可以了,为什么呢?因为通过Order对象测试已经可以掌握整体了。测试可能从未知道ShippingAddress的存在。因为这是单元Order的内部实现细节。

所以,一个类并不是一定是一个好的单元,通常是一组类。单元的定义是为了让你测试,而不管被测试单元内部如何变化,这是单元测试的设计目标,这样你在每次重构时就不必改变测试代码了。

从DDD角度看,Order其实是一个聚合根,也就是说,单元测试中单元是指为每个聚合根做一个测试就可以了。一个DDD aggregate聚合是一组领域对象,能够被看成一个单元就可以了。

在“干净的架构”中有一个用例的概念有所涉及,是通过一组操作来定义的。

在“六边形架构”中有关于一个单元周围被适配器adapters围绕, 这个单元通常称为Middle Hex。

在“DCI”架构中,DHH引述James Coplien的TDD的谈话。James已经小有名气,不仅从他对TDD有过强烈推动和意见,而且更多的他在DCI世界的活动。他是这项运动的创始人之一。 DCI是这里最鼓舞人心的架构。Ruby和DCI其实是一个梦幻般的组合。 DCI提供了良好的工具用于定义一个单位是什么。定义一个上下文为一个单元,一个单元是一组对象的协作的范式。