gpt4 book ai didi

tdd - 如何避免使用TDD创建不良设计

转载 作者:行者123 更新时间:2023-12-03 13:54:26 26 4
gpt4 key购买 nike

我最近(在过去的一周中)开始了一项实验,其中我试图在我正在使用TDD原理进行的项目中编写新功能。过去,我们的方法一直是中度敏捷的方法,但并不严格。方便时,在这里和那里进行单元测试。全面测试覆盖范围的主要障碍是我们的应用程序具有复杂的依赖关系网络。我选择了一项方便进行测试的功能;这些细节并不重要,可能在商业上也很敏感,足以说明这是一个简单的优化问题。

到目前为止,我发现:


对我来说,TDD似乎鼓励形成杂乱无章的设计。禁止未经测试就不得编写代码的限制倾向于阻止将功能分解为独立单元的机会。在实践中很难同时为许多功能编写测试并编写代码
TDD倾向于鼓励创建可以做所有事情的“上帝对象”-因为您已经为类x编写了很多模拟类,但为类y编写了很少的模拟类,因此在当时,类x还应该实现功能z似乎是合乎逻辑的而不是将其交给y类。
在编写代码之前先编写测试,要求您在解决问题之前对问题的每种复杂性有一个完整的了解。这似乎是一个矛盾。
我一直无法让团队开始使用模拟框架。这意味着仅为了测试特定功能而产生的杂物泛滥。对于所测试的每个方法,您都将需要一个伪造品,其唯一的工作就是报告被测类调用了它应该调用的任何东西。我开始发现自己写的类似于DSL的东西纯粹是用于实例化测试数据。
尽管有上述担忧,但TDD制作的工作设计几乎没有神秘的错误,这与我惯用的开发模式不同。重构导致的混乱局面,需要我暂时放弃TDD并完成它。我相信测试将继续加强方法的正确性。尝试进行TDD重构练习,我觉得这只会增加更多的麻烦。


那么问题是:“有人对降低上述担忧的影响有任何建议吗?”。我毫不怀疑,模拟框架将是有利的。但是目前,我已经在运气上尝试一些似乎只是产生乱码的事情。

编辑#1:

谢谢大家的深思熟虑。我承认我在几个星期五晚上喝完啤酒后写了我的问题,所以在某些地方它含糊不清,并没有真正表达出我的初衷。我想强调的是,我喜欢TDD的哲学,并发现它取得了一定程度的成功,但出于我列出的原因,这也令人惊讶。我有机会睡在上面,下周再换一个新的眼睛看问题,所以也许我可以通过摸索解决我的问题。但是,他们都不是初学者。

更令我担心的是,一些团队成员拒绝尝试任何您可以称之为“技术”的事情,而倾向于“仅仅完成它”。我担心,将草屑的外观视为该过程的一个污点,而不是证明必须完全(例如使用模拟框架和强大的DI)完成该过程才能获得最佳结果。

RE“ TDD不一定意味着测试优先” :(糟糕,糟糕)

我在该问题上发现的每一篇文章中的“黄金法则”都是“红色,绿色,重构”。那是:


编写一个必须失败的测试
编写通过测试的代码
重构代码,使其以最实际的方式通过测试


我对如何想象在不遵循最初编写的TDD核心原理的情况下进行测试驱动开发感到好奇。我的同事将中途之家(或另一种同样有效的方法,取决于您的观点)称为“测试验证的开发”。在这种情况下,我认为创造一个新术语-或从其他人那里窃取它并获得信誉-是有用的。

RE DSL用于测试数据:(Michael Venable)

我很高兴你这么说。我确实看到通用形式在项目范围内越来越有用,因为所讨论的应用程序维护着一个相当复杂的对象图,通常,测试它意味着运行该应用程序并在GUI中进行尝试。 (由于上述出于商业敏感性的原因,不会放弃游戏,但从根本上讲,它与有向图上各种指标的优化有关。但是,涉及许多警告和用户可配置的小部件。)

能够以编程方式设置有意义的测试用例将在各种情况下均会有所帮助,可能不仅限于单元测试。

RE神的对象:

我之所以这样,是因为一堂课似乎占据了大部分功能。也许这很好,而且确实很重要,但是它引起了一些注意,因为它看起来像不是按照这些思路开发的较旧的代码,并且似乎违反了SRP。我想这是不可避免的,有些类将主要充当许多不同的封装功能之间的接缝,而另一些只会接缝一些。如果是这样,我想我需要做的是从这个明显的上帝对象中清除尽可能多的逻辑,并将其行为重塑为所有分解因子之间的交汇点。

(对主持人:由于评论字段的长度不足以包含我想要的详细信息,因此我已将我的回复添加到此处的帖子中。)

编辑#2(大约五个月后):

好吧,在考虑了一段时间后,我觉得最好再多加一些思想。

很遗憾,我最终还是放弃了TDD方法。但是,我认为这样做有一些特定而合理的原因,并且我都准备在下次有机会的时候继续进行下去。

TDD毫无道理的重构心态的结果是,当我对代码进行简要介绍时,首席开发人员宣布绝大多数代码毫无意义并且需要执行,我并没有因此而感到沮丧。尽管不得不放弃大量艰苦的工作感到遗憾,但我确实明白了他的意思。

之所以出现这种情况,是因为我从字面上接受了“将代码编码到接口”的规则,但继续编写试图代表现实的类。很久以前,我首先发表了声明:

类不应试图代表现实。对象模型只能尝试解决当前的问题。

...此后,我已尽可能多地重复;对我自己和其他会听的人。

此行为的结果是执行功能的类的对象模型,以及重复了类功能的接口的镜像集。在向我指出这一点之后,经过短暂但激烈的抵制之后,看到了曙光,并且删除了其中的大部分都没有问题。

这并不意味着我相信“接口代码”是多余的。它的意思是,当接口表示真实的业务功能时,对接口进行编码首先很有价值,而不是看起来像现实生活中的微型副本的某些想象中的完美对象模型的属性,但并不考虑其唯一含义。生活中正在回答您最初提出的问题。 TDD的优势在于,除非偶然,否则它无法生成此类模型。因为它从询问问题开始,并且只关心获得答案,所以您的自我和系统先验知识都不会涉及。

我现在正在闲逛,所以我最好做到这一点,并声明我非常想再次尝试TDD,但是对可用的工具和策略有一个更好的概述,并将尽我最大的努力决定我如何想在跳进去之前先做一下。也许一旦我有更多要说的话,我就应该把这个华夫饼移植到它所属的博客中。

最佳答案

就像对您遇到的问题做出全面的回应一样,听起来您使用TDD的时间很长,您可能没有使用任何有助于TDD流程的工具,并且您正在为TDD增值生产线代码比测试线代码高。

更具体地讲:

1:TDD鼓励设计的内容不多于或少于其要求,也就是“ YAGNI”(您将不需要它)方法。那就是“轻装上阵”。您必须通过“正确地做”来平衡它,这是将正确的SOLID设计概念和模式纳入系统中。我遵循以下经验法则:在第一次使用一行代码时,使其起作用。在对该行的第二个引用上,使其可读。在第三,使其成为固体。如果一行代码仅由另一行代码使用,那么当时进行完全SOLID设计,将代码分解为一个接口抽象的类(可以插入和交换)就没有多大意义了。出来。但是,一旦代码开始获得其他用途,就必须具有回溯和重构代码的能力。 TDD和敏捷设计只涉及重构。这是磨擦; Waterfall也是如此,这只是花费更多,因为您必须一直回到设计阶段进行更改。

2:同样,这是纪律单一责任原则:一个对象应该做一件特定的事情,并且是系统中唯一可以做那件事的对象。 TDD不允许您偷懒;它只是帮助您找出可以懒惰的地方。另外,如果您需要创建一个类的很多局部模拟,或者很多功能齐全的完整模拟,那么您可能会错误地构造对象和测试。您的对象太大,您的SUT有太多的依赖关系,并且/或者您的测试范围太广。

3:不,不是。它要求您在编写测试套件时考虑一下您将需要什么。这就是ReSharper(用于MSVS)这样的重构助手真正发挥作用的地方。 Alt + Enter是您的“执行”快捷键。假设您正在开发一个新类,它将写出一个报告文件。您要做的第一件事是新建该类的实例。 ReSharper抱怨“等等”,“我找不到那个班级!”。您说,“按此创建”,按Alt + Enter。确实如此;您现在有一个空的类定义。现在,您在测试中编写了一个方法调用。 ReSharper喊道:“等等,该方法不存在!”,然后再按一次Alt + Enter,然后说“然后创建它”。您只是通过测试进行编程;您具有新逻辑的骨架结构。

现在,您需要一个文件来写入。您首先输入文件名作为字符串文字,知道当RS抱怨时,您可以简单地告诉它将参数添加到方法定义中。等等,这不是单元测试。这需要您创建的方法来接触文件系统,然后必须取回文件并仔细检查文件以确保它是正确的。因此,您决定改为传递Stream;允许您传递完全与单元测试兼容的MemoryStream。 TDD在哪里影响设计决策?在这种情况下,决定是让该类预先更坚固,以便对其进行测试。相同的决定使您可以灵活地将来将数据传输到任何位置。内存,文件,网络,命名管道等。

4:敏捷团队通过协议进行编程。如果没有协议,那是一个障碍。如果团队受阻,则不应编写任何代码。要解决此问题,团队负责人或项目经理要做出命令决策。除非证明是错误的,否则该决定是正确的;如果最终以错误告终,它应该迅速采取行动,这样团队就可以朝新的方向发展而不必回溯。在您的特定情况下,请您的经理做出决定-Rhino,Moq,无论如何-并执行该决定。这其中的任何一项都比手写测试模型好1000%。

5:这应该是TDD的真正优势。你有一堂课;它的逻辑是一团糟,但它是正确的,您可以通过运行测试来证明这一点。现在,您开始重构该类以使其更加可靠。如果重构没有改变对象的外部接口,则测试甚至不必改变;您只是清理了一些测试方法不在乎的方法逻辑,除了它可以工作之外。如果您确实更改了接口,那么您将更改测试以进行不同的调用。这需要纪律;仅仅删除一个不再起作用的测试非常容易,因为被测试的方法不存在。但是,您必须确保对象中的所有代码仍然得到适当执行。代码覆盖率工具可以在此提供帮助,并且可以将其集成到CI构建过程中,如果覆盖率不高,则可以“破坏构建”。但是,承保范围的另一面实际上是双重的:首先,为承保范围而增加承保范围的测试是没有用的;每个测试都必须证明代码在某些新颖的情况下能够按预期工作。另外,“ coverage”不是“ exercise”;您的测试套件可能会执行SUT中的每一行代码,但它们可能无法证明逻辑行在每种情况下均有效。

综上所述,当我初次学习TDD时,会教给我什么,而不会给我带来什么。这是一个编码道场;任务是编写一个罗马数字解析器,该解析器将使用罗马数字字符串并返回一个整数。如果您了解罗马数字的规则,那么可以很容易地预先设计并可以通过给定的任何测试。但是,TDD学科可以很容易地创建一个类,其中包含测试中指定的所有值及其整数的字典。它发生在我们的道场。这是磨擦;如果解析器的实际要求是仅处理我们测试的数字,那么我们没有做错任何事情;系统“运行”,我们没有浪费任何时间来设计在一般情况下可以运行的更复杂的功能。但是,我们新来的Agilites看着泥沼,说这种方法很愚蠢。我们“知道”它必须更智能,更强大。但是我们呢?这是TDD的力量和弱点。您只能设计或满足满足用户要求的系统,因为您不应该(而且经常不能)编写不满足或证明付款人提供的系统对您的某些要求的代码。账单。

尽管我做了很多开发后测试的写作,但是这样做还是有一个主要问题。您已经编写了生产代码,并希望以其他方式对其进行测试。如果现在测试失败,谁错了?如果是测试,则可以更改测试以断言该程序当前正在输出的内容正确。嗯,这没什么用;您刚刚证明系统输出了它一直具有的功能。如果是SUT,那么您会有更大的问题;您有一个已经完全开发并无法通过新测试的对象,现在您必须将其打开并进行更改以使其成功。如果这是您迄今为止唯一的自动测试对象,谁知道您要通过此测试会付出什么努力?相反,TDD会迫使您在合并将通过该测试的任何新逻辑之前编写测试,因此您将获得可证明回归的代码;在开始添加新代码之前,您有一套测试可以证明代码满足当前要求。因此,如果在添加代码时现有测试失败,则可能会导致某些问题,并且除非通过所有已经存在的测试以及所有新测试,否则您不应该提交该代码以进行发行。

如果您的测试中有冲突,那是一个障碍。假设您有一个证明给定方法在给定A,B和C的情况下返回X的测试。现在,您有一个新要求,并且在开发测试时,您发现当给定A,B和C时,相同的方法必须输出Y。 C.好的,先前的测试是证明系统按旧方式运行所不可或缺的,因此更改该测试以证明它现在返回Y可能会破坏基于该行为的其他测试。要解决此问题,您必须弄清楚新要求是对旧要求的行为更改,或者是从接受要求中错误地推断出其中一种行为。

关于tdd - 如何避免使用TDD创建不良设计,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6230026/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com