进行TDD的人们在进行大型重构时如何处理工作流失


37

一段时间以来,我一直在尝试学习为我的代码编写单元测试。

最初,我开始做真正的TDD,在这里我不会写任何代码,除非先编写一个失败的测试。

但是,最近我要解决一个棘手的问题,其中涉及很多代码。在花了几周时间编写测试然后编写代码之后,我得出了一个不幸的结论,即我的整个方法都行不通,我不得不花两周的时间重新开始。

刚编写代码时,这是一个非常糟糕的决定,但是当您还编写了数百个单元测试时,将其全部扔掉在情感上变得更加困难。

我不禁会认为我浪费了3到4天的时间来编写这些测试,因为我本可以将代码放在一起进行概念验证,然后在对方法满意后再编写测试。

练习TDD的人如何正确处理此类情况?在某些情况下是否存在违反规则的情况,或者即使该代码可能看起来毫无用处,您还是总是总是首先刻意编写测试吗?


6
达到完美,不是在没有其他可添加的东西时,而是在没有其他东西可取的时候。
-Antoine

12
您所有的测试怎么可能出错?请解释在变化如何实现你写的每一个测试无效。
S.Lott

6
@ S.Lott:测试没错,它们不再相关。假设您正在使用质数解决问题的一部分,那么您编写一个类来生成质数,并为该类编写测试以确保其正常工作。现在,您找到了另一种完全不同的解决方案,该解决方案完全不涉及素数。该类及其测试现在是多余的。这是我的情况,只有十个班级,而不仅仅是一个班级。
GazTheDestroyer'2

5
@GazTheDestroyer在我看来,区分测试代码和功能代码是一个错误-所有这些都是同一开发过程的一部分。可以公平地注意到,TDD的开销通常可以在开发过程的后续部分进行恢复,在这种情况下,开销似乎并没有给您带来任何好处。但是,同样有多少测试能使您对体系结构的失败有所了解?同样重要的是要注意,您可以(不鼓励)随着时间的推移修剪测试……尽管这可能有点极端(-:
Murph 2012年

10
我将在语义上脚,并在这里同意@ S.Lott;您所做的是不重构,如果它导致丢弃许多类和针对它们的测试。那是重新架构。重构,尤其是在TDD方面,意味着测试是绿色的,您更改了一些内部代码,重新运行了测试,它们一直保持绿色。
埃里克·金

Answers:


33

我觉得这里有两个问题。首先是您没有事先意识到您的原始设计可能不是最佳方法。如果您事先知道这一点,那么您可能已经选择开发一个或两个快速扔掉的原型,以探索可能的设计方案并评估哪种是最有前途的方法。在原型制作中,您无需编写生产质量代码,也不必对每个角落(或根本没有)进行单元测试,因为您的唯一重点是学习而不是完善代码。

现在,意识到您需要原型设计和实验而不是立即开始生产代码的开发并不总是那么容易,甚至也不总是可能。有了刚刚获得的知识,您也许可以认识到下次进行原型制作的需要。也许不是。但是至少您现在知道要考虑此选项。这本身就是重要的知识。

另一个问题是恕我直言,您的看法。我们每个人都会犯错误,并且回想起来很容易看出我们应该做些不同的事情。这就是我们学习的方式。将您的投资写下来作为单元测试的代价,因为学习样机可能很重要,然后克服它。只是努力不犯两次相同的错误:-)


2
我确实知道这将是一个很难解决的问题,并且我的代码有些探索性,但是我最近对TDD的成功充满了热情,所以我像以前一样进行写作测试,因为这就是全部TDD文献讲得太多。所以是的,现在我知道规则可以被打破(这就是我的问题的全部意思),我可能会总结一下。
GazTheDestroyer

3
“我一直像以前那样进行写作测试,因为这是TDD所有文献所强调的。” 您可能应该用想法的源头来更新问题,即所有测试必须在任何代码之前编写。
S.Lott

1
我没有这样的主意,我不确定您是如何从评论中得到的。
GazTheDestroyer

1
我本来打算写一个答案,但是赞成你。是的,是一百万次:如果您还不知道您的体系结构是什么样,请先编写一个废弃的原型,并且不要在原型设计期间费心编写单元测试。
罗伯特·哈维

1
@WarrenP,肯定有人认为TDD是唯一的方法(如果您足够努力,任何事情都可以变成一种宗教;-)。我更喜欢务实。对我来说,TDD是我工具箱中的一种工具,只有在它有助于解决问题而不是阻碍解决问题时,我才使用它。
彼得Török

8

TDD的要点是,它迫使您在小的函数中编写少量的代码增量,正是为了避免此问题。如果您花了数周的时间在一个域上编写代码,并且在重新考虑体系结构时,您编写的每个实用程序方法都变得无用,那么几乎可以肯定,您的方法太大了。(是的,我知道这现在并不能完全安慰rght ...)


3
我的方法根本不算大,考虑到新架构与旧架构完全不同,它们变得无关紧要。部分原因是新架构要简单得多。
GazTheDestroyer

好吧,如果真的没有可重复使用的东西,那么您只能减少损失并继续前进。但是TDD的承诺是,即使您除了编写应用程序代码外,也编写测试代码,它可以使您更快地达到相同的目标。如果这是真的,我坚信是这样,那么至少您达到了在几周内(而不是两倍)就意识到如何进行体系结构的地步。
Kilian Foth

1
@Kilian,关于“ TDD的承诺是它可以使您更快地达到相同的目标”-您在这里指的是什么目标?很明显,与仅编写代码相比,编写单元测试以及生产代码本身会使您开始时变慢。我想说,由于质量的提高和维护成本的降低,TDD只能长期收回投资。
彼得Török

@PéterTörök-有些人坚持认为TDD从来没有任何花费,因为在您编写代码时,TDD可以收回成本。我当然不是这种情况,但基利安似乎自己相信。
psr

好吧...如果您不相信,实际上,如果您不相信TDD具有可观的回报而不是成本,那么这样做完全没有意义,对吗?不仅在加兹描述的非常具体的情况下,而且在所有情况下。恐怕我现在已经完全使该主题偏离主题了:(
Kilian Foth,2012年

6

布鲁克斯说:“计划扔掉一个;不管怎么说,你会的。” 在我看来,您正在这样做。就是说,您应该编写单元测试来测试代码单元,而不要测试大量的代码。这些是更多的功能测试,因此应该在任何内部实现中使用。

例如,如果我想编写一个PDE(偏微分方程)求解器,我将编写一些测试来尝试解决可以用数学方法求解的问题。这些是我的第一个“单元”测试-阅读:功能测试作为xUnit框架的一部分运行。这些不会改变,这取决于我使用哪种算法来求解PDE。我只关心结果。第二个单元测试将集中于用于对算法进行编码的功能,因此将是特定于算法的-如Runge-Kutta。如果我发现Runge-Kutta不适合,那么我仍然会进行那些顶级测试(包括那些表明Runge-Kutta不适合的测试)。因此,第二次迭代仍将具有许多与第一次迭代相同的测试。

您的问题可能与设计有关,而不一定与代码有关。但是没有更多细节,很难说。


它只是外围设备,但是PDE是什么?
CVn 2012年

1
@MichaelKjörling我想这是偏微分方程
foraidt 2012年

2
布鲁克斯不是在第二版中撤回了这一声明吗?
西蒙(Simon)

您如何表示仍然会有显示Runge-Kutta不适合的测试?这些测试是什么样的?您是说您在发现不适合使用的Runge-Kutta算法之前就保存了它,并且在混合使用RK的情况下运行端到端测试会失败吗?
moteutsch 2014年

5

您应该记住,TDD是一个迭代过程。编写一个小测试(在大多数情况下,几行就足够了)并运行它。测试应该失败,现在可以直接在您的主要源上工作,并尝试实现已测试的功能,以便测试通过。现在重新开始。

您不应尝试一次性编写所有测试,因为正如您所注意到的那样,这将无法解决。这样可以减少浪费时间编写不会使用的测试的风险。


1
我认为我不能很好地解释自己。我确实反复编写测试。这就是我最终对突然变得多余的代码进行了数百次测试的结果。
GazTheDestroyer'2

1
如上所述-我认为应该将其视为“测试代码”,而不是“代码测试”
Murph 2012年

1
+1:“您不应该尝试一次性编写所有测试”,
S.Lott 2012年

4

我想您是自己说的:开始编写所有单元测试之前,您不确定自己的方法。

通过比较我所研究的真实TDD项目(实际上并不多,只有3个项目,涵盖了2年的工作),我学到的东西是自动化测试!=单元测试(当然不是相互的)独家)。

换句话说,TDD中的T不一定要带有U ...它是自动化的,但是它比自动化功能测试少一些单元测试(如在测试类和方法中一样):它处于同一级别功能粒度与您当前正在使用的体系结构有关。您从高层次开始,只进行了很少的测试,仅进行了功能的概述,最终最终您最终获得了数千个UT,并且所有类都在漂亮的体系结构中进行了良好定义。

团队合作时,单元测试会为您提供大量帮助,以避免代码更改造成无休止的错误周期。但是在开始为项目工作之前,我从来没有写过那么精确的文章,而对于每个用户故事,至少要有一个全局的工作POC。

也许这只是我个人的方式。我没有足够的经验来从头决定我的项目将具有哪种模式或结构,因此,从一开始我就不会浪费我的时间来编写100个UT ...

更笼统地说,打破一切并扔掉一切的想法将永远存在。我们可以尝试使用我们的工具和方法来保持“连续”,有时,对抗熵的唯一方法是重新开始。但是我们的目标是,当这种情况发生时,您实施的自动化和单元测试将使您的项目已经比没有该项目的项目成本更低-而如果您找到平衡的项目,则成本将会降低。


3
好说-它是TDD,而不是UTDD
Steven A.

极好的答案。根据我对TDD的经验,重要的一点是,书面测试应侧重于软件的功能行为,而不要进行单元测试。从类中考虑您需要的行为比较困难,但这确实会导致界面整洁,并有可能简化最终的实现(您不需要添加不需要的功能)。
JohnTESlade

4
练习TDD的人如何正确处理此类情况?
  1. 通过考虑何时原型和何时编码
  2. 通过意识到单元测试与TDD不同
  3. 通过编写TDD测试来验证功能/故事,而不是功能单元

单元测试与测试驱动的开发的结合是许多痛苦和灾难的根源。因此,让我们再次进行审查:

  • 单元测试涉及验证在每个单独的模块和功能的实现 ; 在UT中,您会看到代码覆盖率指标和测试执行非常迅速的情况
  • 测试驱动开发涉及验证在每个功能/故事的要求 ; 在TDD中,您会看到一些重点,例如首先编写测试,确保编写的代码没有超出预期的范围以及重构质量

总结:单元测试以实现为重点,TDD以需求为重点。它们不是同一件事。


我完全不同意“ TDD关注需求”。您在TDD 编写的测试单元测试。他们确实验证了每个功能/方法。TDD 确实强调代码覆盖率,并且确实关心快速执行的测试(最好这样做,因为您每30秒左右运行一次测试)。也许您在想ATDD或BDD?
guillaume31

1
@ ian31:UT和TDD合并的完美示例。必须不同意并转给您一些原始资料en.wikipedia.org/wiki/Test-driven_development-测试的目的是定义代码要求。BDD很棒。从未听说过ATDD,但是乍一看它确实像我如何应用TDD 缩放比例
史蒂文·劳

您可以完美地使用TDD设计与需求或用户故事没有直接关系的技术代码。在网上,书籍和会议上,您会发现无数的示例,包括发起TDD并将其普及的人们。TDD是一门学科,一种用于编写代码的技术,它会根据您所使用的上下文而停止成为TDD。
guillaume31 '02

另外,您在Wikipedia文章中提到:“测试驱动开发的高级实践可能会导致ATDD,将客户指定的条件自动转换为验收测试,然后驱动传统的单元测试驱动开发(UTDD)流程。 ...]有了ATDD,开发团队现在有一个特定的目标可以满足,即验收测试,这使他们不断专注于客户从该用户故事中真正想要的东西。” 这似乎暗示ATDD主要关注需求,而不是TDD(或他们所说的UTDD)。
guillaume31

@ ian31:OP关于“抛出数百个单元测试”的问题表明规模混乱。如果愿意,可以使用TDD建造棚屋。:D
史蒂文·A·劳

3

测试驱动的开发旨在驱动您的开发。您编写的测试可帮助您断言当前正在编写的代码的正确性,并从第一行开始提高开发速度。

您似乎认为测试是一种负担,并且仅在以后进行增量开发。这种思路与TDD不符。

也许您可以将其与静态类型进行比较:尽管可以在不使用静态类型信息的情况下编写代码,但是将静态类型添加到代码中有助于声明代码的某些属性,解放思想并专注于重要的结构,从而提高了速度和效率。功效。


2

进行大型重构的问题在于,您可以并且有时会遵循一条路径,使您意识到自己付出的代价超过了咀嚼的余地。巨大的重构是一个错误。如果系统设计最初存在缺陷,那么重构可能只会让您走得那么远,然后才需要做出艰难的决定。要么使系统保持原样并进行解决,要么计划重新设计并进行一些重大更改。

然而,还有另一种方式。重构代码的真正好处是使事情更简单,更易于阅读,甚至更易于维护。在遇到不确定性的问题时,您要提出变更,然后走到更远的地方看看它可能会导致什么,以便更多地了解问题,然后扔掉该峰值,并根据该峰值应用新的重构教你。事实是,只有步伐很小并且重构工作不会超出您首先编写测试的能力,您才能真正确定地改善代码。诱惑在于编写一个测试,然后编写代码,然后再编写更多代码,因为解决方案似乎很明显,但是很快您意识到您的更改将更改更多的测试,因此您需要注意一次只能更改一项。

因此,答案是永远不要使重构成为主要问题。宝贝的步骤。从提取方法开始,然后着眼于删除重复项。然后转到提取类。每一步都一次微小的变化。如果要提取代码,请首先编写测试。如果要删除代码,请删除它并运行测试,然后确定是否将需要任何损坏的测试。一次一个小婴儿的步骤。似乎需要更长的时间,但实际上会大大缩短您的重构时间。

但是现实情况是,每个峰值似乎都可能浪费精力。代码更改有时无处可寻,您发现自己从vcs恢复代码。这只是我们日常工作的现实。但是,只要它能教给您一些知识,就不会浪费每个失败的尖峰。每次失败的重构工作都会告诉您,您要么尝试做得太快,要么做错了。如果您从中学到一些东西,那也不会浪费时间。您做得越多,您学到的内容就会越多,并且您将获得更高的效率。我的建议是现在就穿,少做多做,并接受这可能是事情的方式,直到您能够更好地识别出尖峰之前,您才能将它弄到无处可去。


1

我不确定三天后您的方法有缺陷的原因。根据架构的不确定性,您可以考虑更改测试策略:

  • 如果您不确定性能,则可能要从一些声称性能的集成测试开始?

  • 当您正在研究API复杂性时,编写一些真正的裸机小单元测试,以找出解决问题的最佳方法。不要为实现任何事情而烦恼,只需让您的类返回硬编码值或使它们抛出NotImplementedExceptions。


0

对我来说,单元测试也是将接口置于“真实”使用状态的一种机会(就像真实的单元测试一样!)。

如果我被迫进行测试,则必须进行设计。这有助于保持事物的理智(如果某件事太复杂以至于为此编写测试很麻烦,那么使用它会是什么样?)。

这并不能避免更改设计,而是暴露了对它们的需求。是的,完全重写是很痛苦的。为了避免这种情况,我通常会在Python中建立(一个或多个)原型(可能在c ++中进行最终开发)。

当然,您并非总是有时间来享用所有这些好东西。这些都是准确的情况下,当你需要一个LARGER的时间来实现自己的目标......和/或保持一切尽在掌握。


0

欢迎来到有创意的开发商马戏团


不要一开始就遵循所有“合法/合理”的编码方式,而应该
尝试直觉,尤其是对于您而言重要而又新的事物,并且如果没有样本适合您的情况:

-用直觉,从已经知道的事情开始写作,而不是您的思想和想象力。
-停下来
-拿放大镜检查所有写的单词:写“文本”是因为“文本”接近字符串,但是需要“动词”,“形容词”或更准确的内容,请重新阅读并以新的意义调整方法
。 ..或者,您编写了一段考虑未来的代码?删除它
-纠正,执行其他任务(体育,文化或企业以外的其他事情),再回来阅读一遍。
-都很好
-更正,执行其他任务,返回并再次阅读。
-一切都很好,传递给TDD-
现在一切正确,很好
-尝试进行基准测试以指出要优化的事情,然后做。

出现的情况:
-您编写了遵守所有规则的代码
-您获得了经验,一种新的工作方式,
-您的想法有所改变,您永远不会害怕新的配置。

现在,如果您看到的是类似上面的UML,您将能够说
“老板,我是从TDD开始的……。”
这是另一件事吗?
“老板,在决定编码方式之前,我会先尝试一下。”

PARIS
Claude的问候

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.