关于架构:测试驱动开发是否将重点放在设计上?

关于架构:测试驱动开发是否将重点放在设计上?

Does Test Driven Development take the focus from Design?

我对TDD感触良多。虽然我相信测试,但是我对测试驱动开发工作的想法存有疑问。

当您编写代码以满足某些为接口而编写的测试以满足当前需求时,您可能会将重点从构建可维护的代码,从简洁的设计和声音体系结构转移。

我有一个驱动问题,而不是测试问题。有什么想法吗?


编号

如果做对了,测试驱动开发就是您的设计工具。

我希望您能原谅我链接到我自己的博客条目的原因,其中讨论了测试驱动开发的陷阱,这些陷阱之所以出错,仅是因为开发人员将他们的测试仅当作测试。

在先前的项目中,开发人员使用了破坏性很强的单例模式,该模式在整个项目中都实施了依赖关系,而当需求发生变化时,这种依赖关系就破坏了一切:

TDD was treated as a task, when it
should have been treated as an an
approach. [...]

There was a failure to recognize
that TDD is not about tests, it’s
about design. The rampant case of
singleton abuse in the unit tests made
this obvious: instead of the test
writers thinking"WTF are these
singleton = value; statements doing in
my tests?", the test writers just
propagated the singleton into the
tests. 330 times.

The unfortunate consequence is that
the build server-enforced testing was
made to pass, whatever it took.

正确执行"测试驱动开发"应该使开发人员高度了解设计陷阱,例如紧密耦合,违反DRY(不要重复自己),违反SRP(单一责任原则)等。

如果您为了通过测试而为测试编写通过代码,则您已经失败了:您应该将难以编写测试的路标视为让您问的路标:为什么这样做呢?为什么不能不依靠其他代码来测试此代码?为什么我不能重用此代码?为什么单独使用此代码会导致中断?

除了您的设计是否真正干净,代码是否真正可维护之外,为什么为它编写测试并不容易?


始终存在过度执行TDD设计或前期设计的风险。因此,答案是取决于情况。我更喜欢从用户故事/验收测试开始,这是我的测试将有助于产生的要求的基础。只有在确定了这一点之后,我才开始编写TDD风格的详细单元测试。如果您唯一的设计和思考是通过TDD进行的,那么您将冒太多自下而上的方法的风险,这可能会给您带来出色的隔离性的单元和类,但是当您尝试将它们集成到用户故事执行任务中时,做错了一切可能会感到惊讶。有关更多灵感,请查看BDD。

在罗伯特·C·马丁和詹姆斯·科普利恩之间已经记录了有关此问题的一个很好的"辩论",前者是TDD的拥护者,而后者则表示破坏了系统的设计。这是罗伯特对TDD和设计的评价:

"There has been a feeling in the Agile
community since about '99 that
architecture is irrelevant, we don't
need to do architecture, all we need
to do is write a lots of tests and do
lots of stories and do quick
iterations and the code will assemble
itself magically, and this has always
been horse shit. I even think most of
the original Agile proponents would
agree that was a silliness."

James Coplien指出,仅从TDD驱动设计会有很大的风险:

"One of the things we see a lot, in a
lot of projects, is that projects go
south on about their 3rd sprint and
they crash and burn because they
cannot go any further, because they
have cornered themselves
architecturally. And you can't
refactor your way out of this because
the refactoring has to be across class
categories, across class hierarchies,
and you no longer can have any
assurances about having the same
functionality."

他还举了一个很好的例子,说明了与使用先验知识来驱动架构相比,如果您进行测试驾驶,银行帐户的外观将如何:

"I remember when I was talking with
Kent once, about in the early days
when he was proposing TDD, and this
was in the sense of YAGNI and doing
the simplest thing that could possibly
work, and he says: 'Ok. Let's make a
bank account, a savings account.'
What's a savings account? It's a
number and you can add to the number
and you can subtract from the number.
So what a saving account is, is a
calculator. Let's make a calculator,
and we can show that you can add to
the balance and subtract from the
balance. That's the simplest thing
that could possibly work, everything
else is an evolution of that.

If you do a real banking system, a
savings account is not even an object
and you are not going to refactor your
way to the right architecture from
that one. What a savings account is,
is a process that does an iteration
over an audit trail of database
transactions, of deposits and interest
gatherings and other shifts of the
money. It's not like the savings
account is some money sitting on the
shelf on a bank somewhere, even though
that is the user perspective, and
you've just got to know that there are
these relatively intricate structures
in the foundations of a banking system
to support the tax people and the
actuaries and all these other folks,
that you can't get to in an
incremental way. Well, you can,
because of course the banking industry
has come to this after 40 years. You
want to give yourself 40 years? It's
not agile."

有趣的是,TDD的支持者和TDD的反对者都说您需要预先设计。

如果有时间,请观看视频。这是两位很有影响力的专家之间的一次很好的讨论,而且只有22分钟之久。


我完全同意pjz。没有一种正确的方法来设计软件。如果您将TDD推到了极致,除了下一个单元测试之外没有其他任何考虑,则可能会使您的工作变得更艰难。同上一个花了几个月时间在图表和文档上但没有代码的人,开始了一个大型软件项目。

适中。如果有想绘制一个快速图表来帮助您可视化代码结构的需求,那就去做吧。如果需要两页,可能是时候开始编写一些代码了。如果您想在编写测试之前这样做,那该怎么办。我们的目标是提供高质量的软件,而不是绝对符合任何特定软件开发原则。做对您和您的团队有用的事情。寻找可以改进的地方。重复


我考虑的方式是,首先编写您想要的代码。
一旦有了目标代码的样本(现在什么都不做),看看是否可以在其上放置测试支架。
如果无法做到这一点,请找出为什么不能这样做。
在大多数情况下,这是因为您做出了错误的设计决策(99%),但是如果不是这样(1%),请尝试以下操作:

  • 确定您需要遵守哪些疯狂的要求,以免测试您的代码。您可以了解问题,重新设计您的API。
  • 如果其他人决定此要求,请与他/她讨论。他们可能有充分的理由提出要求,一旦您知道了他们的理由,就可以完善您的设计并使之可测试。如果现在还不能,那么您都可以重新设计需求,并且两者都将变得更好。

获得目标代码和测试支架之后。实施代码。现在,您甚至拥有了通过自己的测试时就知道自己的进步程度的优势(这是一个很好的动力!)

根据个人经验,测试可能是多余的唯一情况是当您制作早期原型时,因为那时您仍然对问题的理解不够,无法准确地设计或测试代码。


在这个问题上,我完全同意你的看法。在实践中,我认为TDD通常会对代码库产生非常负面的影响(糟糕的设计,过程代码,没有封装,生产代码中充斥着测试代码,无处不在的接口,难以重构的生产代码,因为所有内容都与许多测试紧密相关)。 )。

吉姆·科普林(Jim Coplien)现在就这个话题进行了一段时间的演讲:

Recent studies (Siniaalto and
Abrahamsson) of TDD show that it may
have no benefits over traditional
test-last development and that in some
cases has deteriorated the code and
that it has other alarming (their
word) effects. The one that worries me
the most is that it deteriorates the
architecture.
--Jim's blog

Robert C. Martin和James Coplien之间也就InfoQ进行了讨论,他们讨论了这个问题。


完成软件需要完成三个步骤:

  • 让它起作用
  • 改正它
  • 快一点
  • 测试使您排名第一。您的代码不仅仅因为测试通过而完成。在开始编写测试/代码之前,最好具有项目结构的一些概念(实用程序,常用访问的对象,层,框架)。在编写代码以使测试通过之后,您需要重新评估它,以查看可以将哪些部分重构为应用程序的不同方面。 Yuo可以自信地做到这一点,因为您知道只要测试仍在通过中,您的代码就仍然可以正常工作(或至少满足要求)。

    在项目开始时,请考虑一下结构。随着项目的进行,请继续评估和重新评估您的代码,以保持设计不变,或者在不再有意义的情况下更改设计。在估算时,必须考虑所有这些项目,否则最终将得到意大利面条字代码(无论是否为TDD)。


    这里有许多非正式的意见,包括流行的意见(来自Jon Limjap),认为做错事会导致不好的结果,并且声称除了个人经验以外,似乎并没有其他依据。大量的经验证据和已发表的结果与该经验指向相反的方向。

    理论上认为,一种要求您在代码之前编写测试的方法将导致在单个代码段(即,小型编程)级别上进行设计的思考。由于过程就是您可以测试的全部内容(您仍然一次只能测试一个方法的一个对象,而您根本无法测试大多数语言的类),因此您的设计重点将放在各个方法及其组合方式上。从理论上讲,这导致了自下而上的程序设计,进而导致对象之间的不良耦合和内聚。

    广泛的经验数据证实了该理论。 Siniaalto和Abrahamsson(测试驱动开发对程序设计和测试覆盖率影响的比较案例研究),ESEM 2007,发现"我们的结果表明内聚性可能更差(即使Beck声称TDD产生了高内聚性系统)在我们的第二项研究中,我们注意到使用TDD的复杂性度量更好,但是依赖管理指标显然更差。" Janzen和Saledian(测试驱动开发真的提高了软件设计质量吗?IEEE软件25(2),2008年3月/ 4月,第77-84页)发现,"该结果不支持较低耦合度的主张,并且与TDD的内聚力增强"。

    文献综述将发现其他出版物,进一步加剧这些情况。

    即使我亲爱的朋友Bob叔叔也写道:"敏捷开发的一个更加隐蔽和持久的神话是,前期的架构和设计很糟糕;您永远不要花时间在前期进行架构决策。相反,您应该一次只用一个测试用例就一无所有地发展您的架构和设计。对不起,但这就是马屎。" ("敏捷架构的类别学",
    http://blog.objectmentor.com/articles/2009/04/25/the-scatology-of-agile-architecture)

    但是,值得注意的是,更广泛的失败是人们认为这是一种测试技术,而不是设计技术。 Osherov指出了许多通常与TDD等同的方法。我不确定这里的海报是什么意思。请参阅:http://weblogs.asp.net/rosherove/archive/2007/10/08/the-various-意义-of-tdd.aspx。


    在大多数情况下,我同意TDD确实提供了一种设计工具。对我来说,最重要的部分是它构建方式进行更多更改的能力(您知道,当您拥有洞察力的时刻,可以通过删除代码来添加功能),大大降低了风险。

    也就是说,我最近从事的一些算法工作在TDD下受到了一些折磨,而没有仔细考虑设计思想。上面关于更安全的重构的声明仍然是一个很大的好处,但是对于某些算法,TDD(尽管仍然有用)不足以使您获得理想的解决方案。以排序为例。 TDD可以很容易地导致您获得次优(N ^ 2)算法(以及大量通过的测试,使您能够重构为快速排序的算法),例如冒泡排序。 TDD是一种工具,是非常好的工具,但是就像要解决问题的上下文一样,许多东西都需要适当使用。


    我在TDD和单元测试方面相对较新,但是在我使用过的两个附带项目中,我发现它是设计助手,而不是设计的替代品。独立测试和验证组件/子组件的能力使我更容易进行快速更改并尝试新的设计思路。

    我在TDD上经历的差异是可靠性。在设计过程的开始而不是以后,就可以在较小级别的组件上确定组件接口的过程是,我拥有可以信任的组件,它们可以更早地工作,因此,我不必担心这些小组件而取而代之解决棘手的问题。

    当我不可避免地需要回来维修这些小零件时,我可以花更少的时间这样做,这样我就可以回到想要做的工作上。


    总是平衡的:
    -TDD过多,您最终会得到有效的代码,但是使用起来却很痛苦。
    -太多的"可维护的代码,简洁的设计和良好的体系结构",您最终会遇到使宇航员陷入瘫痪的架构宇航员

    一切都适度。


    推荐阅读