测试驱动开发(通常是敏捷)的这种局限性在实际上是否相关?


30

在测试驱动开发(TDD)中,您从次优解决方案开始,然后通过添加测试用例和重构来迭代地产生更好的解决方案。该步骤应该很小,这意味着每个新解决方案都将以某种方式位于前一个解决方案的附近。

这类似于数学上的局部优化方法,例如梯度下降或局部搜索。这种方法的一个众所周知的局限性是它们不能保证找到全局最优值,甚至不能保证找到可接受的局部最优值。如果您的起点与所有可接受的解决方案之间有很大范围的不良解决方案,则无法到达此目标,该方法将失败。

更具体地说:我正在考虑一个场景,在该场景中,您已经实现了多个测试用例,然后发现下一个测试用例将需要一种完全不同的方法。您将不得不放弃以前的工作,然后重新开始。

这种想法实际上可以应用于所有以小步骤进行的敏捷方法,而不仅仅是TDD。TDD与局部优化之间的拟议类比是否存在严重缺陷?


您是否在指称三角测量的TDD子技术?“可接受的解决方案”是指正确的解决方案还是可维护的/优雅的/可读的解决方案?
guillaume31

6
我认为这是一个真正的问题。由于这只是我的意见,所以我不会写答案。但是,是的,因为TDD被吹捧为一种设计实践,所以它会导致局部最大值或根本没有解决方案,这是一个缺陷。我会说,一般来说,TDD不太适合算法设计。请参阅有关TDD局限性的相关讨论:用TDD解决数独,其中罗恩·杰弗里斯(Ron Jeffries)在圈中奔跑和“做TDD”时自言自语,而彼得·诺维格(Peter Norvig)通过实际了解相关主题提供了实际的解决方案,
Andres F.17年

5
换句话说,我会提供(希望如此)无争议的陈述,即TDD可以最大程度地减少您在“已知”问题中编写的类的数量,从而生成更简洁的代码,但不适用于算法问题或复杂问题,其中实际上,从整体上看并具有特定领域的知识比编写零碎的测试和“发现”您必须编写的代码更有用。
安德列斯·F

2
问题存在,但不仅限于TDD甚至敏捷。不断变化的需求意味着必须始终更改以前编写的软件的设计。
RemcoGerlich

@ guillaume31:不一定是三角剖分,而是在源代码级别使用迭代的任何技术。通过可接受的解决方案我的意思是一个通过了所有测试,并且可以保持相当好..
弗兰克河豚

Answers:


8

这种方法的众所周知的局限性在于它们不能保证找到全局最优值,甚至不能保证找到可接受的局部最优值。

为了使您的比较更加恰当:对于某些问题,迭代优化算法很可能会产生良好的局部最优,而在其他情况下,它们可能会失败。

我正在考虑一个场景,在该场景中,您已经实现了多个测试用例,然后发现下一个测试用例将需要一种完全不同的方法。您将不得不放弃以前的工作,然后重新开始。

我可以想象现实中可能发生这种情况:当您以某种方式选择了错误的体系结构时,您需要从头开始重新创建所有现有的测试。假设您开始在操作系统A上以编程语言X实现您的前20个测试用例。不幸的是,要求21包括整个程序需要在无法使用X的操作系统B上运行。因此,您需要放弃大部分工作并使用语言Y重新实现。(当然,您不会完全放弃代码,而是将其移植到新的语言和系统上。)

这告诉我们,即使使用TDD时,也要事先进行一些总体分析和设计是一个好主意。但是,对于任何其他类型的方法也是如此,因此我不认为这是固有的TDD问题。而且,对于大多数现实世界中的编程任务,您只需选择一种标准体系结构(例如编程语言X,操作系统Y,硬件XYZ上的数据库系统Z),并且可以相对确定地使用了迭代或敏捷方法(如TDD)不会把你带入死胡同。

引用罗伯特·哈维(Robert Harvey)的话:“您不能通过单元测试来发展架构。” 或pdr:“ TDD不仅可以帮助我达到最佳的最终设计,还可以帮助我减少尝试次数。”

所以实际上你写的是

如果您的起点与所有可接受的解决方案之间有很大范围的不良解决方案,那么您将无法到达那里,并且该方法将失败。

可能会变成事实-当您选择错误的体系结构时,很可能无法从那里获得所需的解决方案。

另一方面,当您事先进行一些总体规划并选择合适的体系结构时,使用TDD就像在可能会达到“全局最大值”(或至少足够好的最大值)的区域中启动迭代搜索算法一样。 )在几个周期内。


20

我不认为TDD有局部最大值的问题。正如您已经正确注意到的那样,您编写的代码可能会出现,但这就是为什么要进行重构(在不更改功能的情况下重写代码)的原因。基本上,随着测试的增加,如果需要,您可以重写对象模型的重要部分,同时通过测试保持行为不变。测试有关系统的不变事实,因此需要在局部最大值和绝对最大值中均有效。

如果您对与TDD相关的问题感兴趣,可以提及我经常想到的三个不同的问题:

  1. 完整性问题:有多少检查是必要的,以完整地描述一个系统?“按实例编码”是描述系统的完整方法吗?

  2. 硬化的问题:无论测试接口,需要有一个不变的接口。测试代表不变的真理,切记。不幸的是,对于我们编写的大多数代码,这些真相根本是未知的,充其量仅是面向外部的对象。

  3. 测试损害的问题:为了使断言可测试的,我们可能需要编写代码欠佳(少高性能,例如)。我们如何编写测试,以使代码尽可能地好?


编辑以解决评论:这是通过重构为“ double”函数排除局部最大值的示例

测试1:当输入为0时,返回零

实现方式:

function double(x) {
  return 0; // simplest possible code that passes tests
}

重构:不需要

测试2:当输入为1时,返回2

实现方式:

function double(x) {
  return x==0?0:2; // local maximum
}

重构:不需要

测试3:当输入为2时,返回4

实现方式:

function double(x) {
  return x==0?0:x==2?4:2; // needs refactoring
}

重构:

function double(x) {
  return x*2; // new maximum
}

1
但是,我的经验是,我的第一个设计仅适用于一些简单的情况,后来我意识到我需要一个更通用的解决方案。开发更通用的解决方案需要更多测试,而特殊情况下的原始测试将不再起作用。我发现在开发更通用的解决方案时暂时删除这些测试是可以接受的,并在准备就绪时将其重新添加。
5gon12eder'1

3
我不认为重构是一种泛化代码(当然是在人工“设计模式”空间之外)或逃避局部最大值的方法。重构可以整理代码,但不会帮助您发现更好的解决方案。
安德烈斯·F

2
@Sklivvz理解了,但在您发布的玩具示例之外,我认为这种方式不起作用。同样,它可以帮助您将函数命名为“ double”;以某种方式您已经知道答案了。当您或多或少知道答案但想“干净利落”地写时,TDD绝对有帮助。它对发现算法或编写真正复杂的代码没有帮助。这就是为什么罗恩·杰弗里斯(Ron Jeffries)无法以这种方式解决数独问题的原因。您无法通过TDD使其模糊不清来实现您不熟悉的算法。
安德列斯F.

1
@VaughnCato好吧,现在我处于信任您或持怀疑态度的位置(这很粗鲁,所以我们不要那样做)。只能说,根据我的经验,它不像您说的那样有效。我从未见过从TDD演化出相当复杂的算法。也许我的经验太有限了:)
Andres F.

2
@Sklivvz“只要您可以编写适当的测试”,这就是重点:听起来像是向我求问。我的意思是,你常常不能首先编写测试并不容易考虑算法或求解器。你必须看看全貌第一。当然,必须尝试场景,但是请注意,TDD与编写场景无关:TDD与测试驱动设计有关!您不能通过先编写测试来推动Sudoku求解器(或用于其他游戏的新求解器)的设计。作为轶事证据(还不够):杰弗里斯做不到。
安德列斯·F

13

用数学术语描述的就是我们所说的将自己涂在角落。这种情况几乎不是TDD独有的。在瀑布中,您可以收集并倾倒数月的需求,希望您可以看到全局最大值,然后才能意识到下一个山坡上还有一个更好的主意。

区别在于在敏捷环境中,您从未期望过此刻是完美的,因此您已准备好抛弃旧的想法并转向新的想法。

对于TDD更具体,有一种技术可以防止您在TDD下添加功能时发生这种情况。这是转换优先权前提。TDD有一种正式的方法供您重构,而这是添加功能的一种正式方法。


13

@Sklivvz 在回答中令人信服地指出该问题不存在。

我想指出这并不重要:一般而言,尤其是在敏捷开发(尤其是TDD)中,迭代方法的基本前提(以及存在的理由)不仅是全局最优,而且局部最优也没有。众所周知。因此,换句话说:即使这是一个问题,也没有办法以迭代的方式来解决这个问题。假设您接受了基本前提。


8

TDD和敏捷实践能否承诺提供最佳解决方案?(甚至是一个“好的”解决方案?)

不完全是。但这不是他们的目的。

这些方法简单地提供了从一种状态到另一种状态的“安全通道”,并认识到变化是耗时,困难和冒险的。两种做法的重点是确保应用程序和代码既可行,又被证明可以更快,更定期地满足要求。

... [TDD]反对允许添加未经证明可满足要求的软件的软件开发 ...肯特•贝克(Kent Beck)因开发或“发现”了该技术而倍受赞誉,他在2003年表示,TDD鼓励简单设计并激发信心。维基百科

TDD致力于确保每个“大块”代码都满足要求。尤其是,它有助于确保代码满足预先存在的要求,而不是让要求受不良代码驱动。但是,它不保证实现以任何方式都是“最佳”的。

至于敏捷流程:

工作软件是进度的主要衡量标准。在每次迭代结束时,利益相关者和客户代表都会审查进度并重新评估优先级,以优化投资回报率(Wikipedia

敏捷并不是在寻找最佳解决方案 ; 只是一个可行的解决方案-目的是优化ROI。它承诺工作的解决方案更快,而不是以后 ; 不是“最佳”的。

但是,那还可以,因为这个问题是错误的。

软件开发的最佳目标是模糊的,不断变化的目标。要求通常是不断变化的,并且到处都是秘密,这些秘密只会在充满老板老板的会议室里出现,这使您很尴尬。解决方案的体系结构和编码的“内在优势”是由您的同龄人和您的管理上级主管的分歧和主观意见来分级的-他们都不会对优质软件有任何了解。

在最起码,TDD和敏捷实践承认的困难和尝试优化的两件事情客观的,可衡量的:。工作v未-工作迟早v以后。

而且,即使我们将“工作”和“更快”作为客观指标,您为它们进行优化的能力也主要取决于团队的技能和经验。


您在努力产生最佳解决方案时可能会想到的事情包括:

等等..

这些问题是否真的产生了最佳解决方案,这将是另一个要问的大问题!


1
是的,但我没有写过TDD或其他任何软件开发方法的目标都是全局最优的最优解决方案。我唯一担心的是,在许多情况下,基于源代码级别的小迭代的方法可能根本找不到任何可接受的(足够好的)解决方案
Frank Puffer

@Frank我的答案旨在涵盖局部和全局最优值。两种方法的答案都是:“不,这不是这些策略的设计目标-它们旨在提高ROI和减轻风险。” ... 或类似的东西。这部分归因于约尔格的答案:“最优”是移动的目标。...我什至更进一步;它们不仅是移动的目标,而且还不是完全客观或可衡量的。
svidgen

@FrankPuffer也许值得补遗。但是,最基本的要点是,您要问这两个问题是否实现了它们根本不是设计或不希望实现的目标。更重要的是,您在问他们是否可以实现无法衡量或验证的目标。
svidgen

@FrankPuffer Bah。我试图更新我的答案以使其更好。我不确定我做得更好或更糟!...但是,我需要下车SE.SE并重新开始工作。
svidgen

这个答案是可以的,但是我遇到的问题(与其他一些答案一样)是“减轻风险和提高ROI”并不总是最好的目标。实际上,它们本身并不是真正的目标。当您需要工作时,减轻风险并不会减少它。有时,如TDD中相对无方向的小步骤将不起作用-可以将风险降到最低,但最终不会到达任何有用的地方。
安德列斯F.

4

到目前为止,没有人添加的一件事是您所描述的“ TDD开发”是非常抽象和不现实的。就像在数学应用程序中,您正在优化算法,但是在大多数编码人员从事的业务应用程序中却很少发生。

在现实世界中,您的测试基本上是在行使和验证业务规则:

例如-如果客户是30岁的不吸烟者,有妻子和两个孩子,则溢价类别为“ x”等。

您将不会迭代更改溢价计算引擎,直到很长时间才正确-几乎可以肯定,当应用程序处于活动状态时;)。

您实际创建的是一个安全网,以便为特定类别的客户添加新的计算方法时,所有旧规则都不会突然中断并给出错误的答案。如果调试的第一步是创建一个测试(或一系列测试)以在编写代码以修复错误之前重现错误,则安全网甚至会更加有用。然后,在一年的时间里,如果有人不小心重新创建了原始错误,单元测试甚至在代码签入之前就被打破了。是的,TDD允许的一件事是,您现在可以放心地进行大型重构和整理但这不应该是您工作的重要部分。


1
首先,当我阅读您的答案时,我想“是的,这就是核心”。但是,在再次考虑这个问题之后,我认为它不一定是如此抽象或不现实。如果盲目选择完全错误的架构,TDD不会解决这一问题,除非经过1000次迭代。
布朗

@Doc Brown同意,它将无法解决该问题。但这将为您提供一套测试,这些测试可以运用所有假设和业务规则,以便您可以迭代地改进体系结构。如此糟糕的架构非常罕见,需要进行彻底的重写才能修复(我希望如此),即使在这种极端情况下,业务规则单元测试也将是一个很好的起点。
mcottle '17

当我说“错误的体系结构”时,我想到的情况是需要丢弃现有的测试套件。你读我的答案了吗?
布朗博士

@DocBrown-是的,我做到了。如果您的意思是“错误的体系结构”是“更改整个测试套件”,那么您应该这么说。更改体系结构并不意味着您必须废弃所有基于业务规则的测试。您可能必须更改所有它们以支持您创建的任何新接口,甚至完全重写某些接口,但是业务规则不会被技术更改所取代,因此测试将保留。当然,对单元测试的投资不应因完全没用整个架构的可能性而无效。
mcottle

当然,即使一个人需要用一种新的编程语言重写每个测试,也不需要扔掉所有东西,至少一个人可以移植现有的逻辑。我同意100%用于大型现实项目,问题中的假设是不现实的。
布朗

3

我认为这不会妨碍您。大多数团队都没有人能够提出最佳解决方案,即使您将其写在白板上也是如此。TDD /敏捷不会妨碍他们。

许多项目不需要最佳解决方案,而那些需要最佳解决方案的项目将在此领域中花费必要的时间,精力和精力。像我们倾向于构建的所有其他事物一样,首先要使其运行。然后加快速度。如果性能如此重要,则可以使用某种原型进行此操作,然后利用通过多次迭代获得的智慧来重建整个事情。

我正在考虑一个场景,在该场景中,您已经实现了多个测试用例,然后发现下一个测试用例将需要一种完全不同的方法。您将不得不放弃以前的工作,然后重新开始。

这可能发生,但是更可能发生的是担心更改应用程序的复杂部分。没有任何测试可以在这方面产生更大的恐惧感。TDD并具有一组测试的好处是,您使用需要更改的概念构建了该系统。当您从一开始就提出这种整体式优化解决方案时,很难进行更改。

此外,将其放在您担心优化不足的情况下,您会不禁花时间优化不应有的功能并创建不灵活的解决方案,因为您过于关注其性能。


0

将诸如“局部最优”之类的数学概念应用于软件设计可能是骗人的。使用这样的术语使软件开发听起来比实际要量化和科学得多。即使代码存在“最佳”,我们也无法对其进行度量,因此也无法知道是否已达到目标。

敏捷运动实际上是对人们认为可以使用数学方法来计划和预测软件开发的一种反应。不管好坏,软件开发更像是一门工艺,而不是一门科学。


但是反应太强烈了吗?在很多情况下,严格的前期计划显得笨拙且成本高昂,这无疑会有所帮助。但是,某些软件问题必须作为数学问题来解决,并且需要进行前期设计。您不能TDD他们。您可以TDD Photoshop的UI和整体设计,但不能TDD其算法。它们不是简单的示例,例如在典型的TDD示例[1]中派生“和”,“双精度”或“功率”。您可能无法从编写一些测试方案中获得一个新的图像过滤器;您绝对必须坐下来编写并理解公式。
安德列斯F.

2
[1]实际上,我很确定fibonacci,我已将其用作TDD示例/教程,这或多或少是个谎言。我愿意打赌,没有人会通过TDD'ing来“发现”斐波那契或任何类似的系列。每个人都从已经知道斐波那契开始,这是作弊。如果您尝试通过TDD'ing来发现它,那么您很可能会到达OP所要求的死胡同:您将永远无法仅通过编写更多测试和重构来概括该系列-您必须应用数学方法推理!
安德列斯F.

有两个评论:(1)您说对了,这是对的。但是我没有写TDD 数学优化相同。我只是将其用作类比或模型。我确实相信,只要您知道模型与真实事物之间的差异,数学就可以(并且应该)应用于几乎所有事物。(2)科学(科学工作)通常比软件开发更难以预测。我什至会说软件工程更像是科学作品,而不是工艺。工艺品通常需要更多的日常工作。
Frank Puffer

@AndresF。:TDD并不意味着您不必思考或设计。这只是意味着您在编写实现之前先编写测试。您可以使用算法来做到这一点。
JacquesB

@FrankPuffer:好的,那么在软件开发中具有“局部最优”的可衡量的价值是什么?
JacquesB
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.