TDD与生产率


131

在我当前的项目(一个用C ++编写的游戏)中,我决定在开发过程中将100%使用“测试驱动开发”。

就代码质量而言,这很棒。我的代码从未设计得那么好或没有漏洞。当我查看一年前在项目开始时编写的代码时,我并不畏缩,而且我对如何组织事物有了更好的了解,不仅可以更容易测试,而且可以更容易实现和使用。 。

但是...我开始这个项目已经一年了。当然,我只能在业余时间处理它,但是与我以前相比,TDD仍然使我的速度大大降低。我读到,随着时间的流逝,开发速度的降低会越来越好,而且我确实确实比以前更容易进行测试,但是我已经进行了一年,而且我仍在努力。

每当我考虑需要执行的下一步时,就必须每次都停下来思考如何为它编写测试,以便允许我编写实际的代码。有时我会被困上几个小时,确切地知道我要编写什么代码,却不知道如何将其分解得足够细致,以至于无法完全被测试覆盖。其他时候,我会迅速考虑十几个测试,并花一个小时编写测试,以覆盖一小部分真实的代码,而这些代码原本要花几分钟才能编写。

或者,在完成第50次测试以涵盖游戏中的特定实体及其创建和使用的各个方面之后,我查看了我的待办事项清单,看到了下一个要编码的实体,并为写作而大吃一惊。另进行50次类似测试以使其得以实施。

到了关键点,回顾去年的进展,我正考虑放弃TDD,以“完成该死的项目”。但是,放弃它随附的代码质量并不是我所期望的。恐怕如果我停止编写测试,那么我将失去使代码如此模块化和可测试的习惯。

我是否可能在做错事而仍然如此缓慢?是否有其他选择可以在不完全丧失收益的情况下提高生产率?TAD?较少的测试范围?其他人如何在不破坏所有生产力和动力的情况下在TDD中生存?


@Nairou:您可以随时尝试“完成项目”!现在建立一个分支。只需在其中编写代码。但是,根据时间或游戏实体的数量来限制您的操作,看看您是否走得更快。然后,您可以忽略该分支,从那里返回到主干和TDD,看看有什么区别。
quamrana 2011年

9
对我来说,过早编写测试就像过早优化。您可能正在努力测试将来将要删除的代码。
LennyProgrammers 2011年

我有点担心,您花了很多时间来思考一种设计代码的方法,以使其更具可测试性。可测试性是精心设计的可能属性,但它不应成为设计的首要目标
杰里米

2
早在我学习的时候,我们就知道何时必须提供设计文档。我们首先编写代码,然后编写文档来描述代码。也许您需要为TDD学习适度的实用主义。如果您已经有了一个计划,也许最好在编写测试之前将大部分计划纳入代码中。不管理想主义者提出什么建议,有时候最好做一些自己已经准备好的事情,而不是让自己分心,然后在不再新鲜时再回来。
Steve314,2011年

4
我要与流行观点背道而驰,如果您正在制作游戏,那么TDD可能并不总是正确的选择。由于gamedev.stackexchange上的某人已经很好地回答了这个问题,因此我将在此处链接它
l46kok

Answers:


77

首先,我要感谢您分享您的经验并表达您的担忧……我必须说这并不少见。

  • 时间/生产力:编写测试比不编写测试要慢。如果您将其范围限定于此,我会同意。但是,如果您在应用非TDD方法的情况下进行了并行工作,则很可能花费大量的时间在现有代码的“破译-检测-调试-修复”上会使您陷入困境。对我来说,TDD是我所能走的最快而又不影响我的代码信心的地方。如果您发现自己的方法没有增加价值,请消除它们。
  • 测试次数:如果您编写N个事物,则需要测试N个事物。用肯特·贝克(Kent Beck)的一句话来解释:“ 仅在您希望它起作用的情况下进行测试。
  • 数小时陷入困境:我也这样做(有时,我停下来之前不会超过20分钟)。这只是您的代码告诉您设计需要做一些工作。测试只是您的SUT类的另一个客户端。如果测试发现难以使用您的类型,那么您的生产客户也很可能会使用它。
  • 乏味的类似测试:我需要更多的上下文来提出反驳。就是说,停下来思考一下相似之处。您能以某种方式对这些测试进行数据驱动吗?是否可以针对基本类型编写测试?然后,您只需要针对每个派生运行相同的测试集。听您的测试。保持适当的懒惰,看看是否可以找到避免烦闷的方法。
  • 停止考虑下一步需要做什么(测试/规格)不是一件坏事。相反,建议您构建“正确的东西”。通常,如果我无法想到如何测试它,我通常也不会想到实现。在实现之前,最好先消除实现想法。.也许YAGNI式的抢先设计掩盖了一个更简单的解决方案。

这使我想到了最后一个查询:我如何变得更好?我的答案是阅读,反思和练习。

例如,最近,我一直关注

  • 我的节奏是反映RG [Ref] RG [Ref] RG [Ref]还是RRRRRGRRef。
  • 在“红色/编译错误”状态下花费的时间百分比。
  • 我会陷入红色/损坏的构建状态吗?

1
我非常好奇您对数据驱动测试的评论。您是指一组处理外部数据(例如来自文件)的测试,而不是重新测试类似的代码吗?在我的游戏中,我有多个实体,每个实体都有很大的不同,但是有一些共同的事情要做(通过网络对它们进行序列化,确保它们不会被发送给不存在的玩家,等等)。到目前为止,我还没有找到一种方法来整合这一点,因此,每个测试集几乎是完全相同的,只是它们使用的是什么实体以及包含的数据是不同的。

@Nairoi-不确定您使用的是什么测试跑步者。我刚刚学到一个要传达的名字。抽象夹具模式[ goo.gl/dWp3k]。这仍然需要您编写与具体SUT类型一样多的Fixture。如果您想更加简洁,请查看跑步者的文档。例如,NUnit支持参数​​化和通用测试装置(现在我已经在搜索它了)goo.gl/c1eEQ看起来像您需要的东西。
Gishu 2011年

有趣的是,我从未听说过抽象装置。我目前使用具有夹具的UnitTest ++,但没有抽象夹具。它的工具非常直观,只是一种合并测试代码的方法,对于给定的一组测试,您需要在每个测试中重复这些代码。

@asgeo-无法编辑该评论。.该链接拾起了一个后括号

+1表示“卡住是设计的征兆,需要做更多的工作”,尽管..当您(像我一样)卡住设计时会发生什么?
lurscher 2011年

32

您不需要100%的测试覆盖率。务实。


2
如果您没有100%的测试覆盖率,那么您就没有100%的信心。
Christopher Mahan

60
即使100%的测试覆盖率也没有100%的置信度。那是测试101。测试无法证明该代码没有缺陷。相反,他们只能证明它确实包含缺陷。
CesarGon 2011年

7
就其价值而言,TDD最热情的拥护者之一鲍勃·马丁(Bob Martin)不建议100%覆盖率-blog.objectmentor.com/articles/2009/01/31/…。在制造业(已授予,在许多方面与软件不同)上,没有人会获得100%的信心,因为他们可以花一部分精力来获得99%的信心。
可能有

另外(至少上次我检查了工具时),代码覆盖率报告也与行是否执行有关,但不包括值覆盖率。例如,今天我报告了一个错误,该错误表明我在测试中通过代码执行了所有路径,但是由于有类似这样的行a = x + y,尽管代码中的所有行都在测试中执行,所以测试仅针对y = 0的情况进行测试,因此a = x - y在测试中从未发现错误(应该是)。
皮特·柯坎

@Chance-我读过罗伯特·马丁(Robert Martin)的一本书“ Clean coder ...”。它在那本书中说,应该渐近地100%覆盖测试,接近100%。而且博客链接不再起作用。
Darius.V

22

TDD仍然让我放慢脚步

这实际上是错误的。

如果没有TDD,您将花费几周的时间编写大部分有效的代码,并在第二年花费“测试”并修复许多(但不是全部)错误。

使用TDD,您需要花费一年的时间编写真正有效的代码。然后,您需要进行几周的最终集成测试。

经过的时间可能会是相同的。TDD软件的质量将大大提高。


6
那么,为什么我需要TDD?“经过的时间是相同的”

21
@Peter Long:代码质量。“测试”年是我们最终使用最有效的废话软件的原因。
S.Lott

1
@Peter,你一定是在开玩笑。TDD解决方案的质量将非常优越。
马克·托马斯

7
为什么需要TDD?肯特·贝克(Kent Beck)列出了省心的一个重要方面,这对我来说非常令人信服。当我没有单元测试的代码工作时,我一直担心会破坏东西。

7
@Peter Long:“经过的时间是相同的” ...并且在这段时间中的任何时候,您都可以交付工作代码
Frank Shearar 2011年

20

或者,在完成第50次测试以涵盖游戏中的特定实体及其创建和使用的各个方面之后,我查看了我的待办事项清单,看到了下一个要编码的实体,并为写作而大吃一惊。另进行50次类似测试以使其得以实施。

这让我想知道您要执行TDD的“重构”步骤。

当所有测试通过时,是时候重构代码并删除重复项了。人们通常会记住这一点,但有时他们会忘记这也是重构测试,消除重复并简化事情的时候。

如果您有两个实体合并为一个实体以实现代码重用,则也考虑合并其测试。您实际上只需要测试代码中的增量差异。如果您不定期对测试进行维护,则测试很快就会变得笨拙。

关于TDD的几个哲学观点可能会有所帮助:

  • 尽管您有写测试的丰富经验,但如果您仍然不知道如何编写测试,那绝对是一种代码味道。您的代码某种程度上缺乏模块化,这使得编写小型简单测试变得困难。
  • 使用TDD时,完全可以接受一些代码。编写所需的代码,以了解其外观,然后删除代码并从测试开始。
  • 我认为练习非常严格的TDD是一种锻炼方式。刚开始时,一定要每次都先编写一个测试,然后编写最简单的代码使测试通过,然后再继续。但是,一旦您习惯了这种做法,就不必这样做了。我没有针对我编写的每个可能的代码路径进行单元测试,但是通过经验,我能够选择需要通过单元测试进行测试的内容,以及可以由功能或集成测试覆盖的内容。如果您已经严格按照一年的时间来练习TDD,那么我想您也已经接近这一点了。

编辑:关于单元测试的哲学主题,我认为这对您来说可能很有趣:Testivus的方式

还有一个更实际的,即使不一定很有帮助的观点:

  • 您提到C ++作为您的开发语言。我已经使用JUnit和Mockito等出色的库在Java中广泛地实践了TDD。但是,由于缺乏可用的库(尤其是模拟框架),我发现C ++中的TDD非常令人沮丧。尽管这一点对您当前的状况没有太大帮助,但我希望您在完全放弃TDD之前先考虑到这一点。

4
重构测试很危险。似乎没有人在谈论这个,但是事实是这样。我当然没有单元测试来测试我的单元测试。当您进行重构以减少重复时,通常会增加复杂度(因为代码变得更加通用)。这意味着您的测试中更有可能出现错误。
Scott Whitlock

2
我不同意重构测试是危险的。您只有在一切都通过时才进行重构,所以如果您进行重构并且一切仍然是绿色的,那么您就可以了。如果您认为需要为测试编写测试,那么我觉得这表明您需要编写更简单的测试。
jaustin 2011年

1
C ++很难进行单元测试(该语言无法轻松地使模拟变得容易)。我注意到作为“函数”的函数(仅对参数进行操作,结果作为返回值/参数显示)比“过程”(返回void,无参数)更容易测试。我发现,对精心制作的模块化C代码进行单元测试实际上比C ++代码更容易。您不必用C编写,但是可以遵循模块化C的示例。听起来完全是疯了,但是我已经将单元测试放在了“坏C”上,那里的所有东西都是全局的,而且超级容易-所有状态始终可用!
匿名

2
我认为这是真的。我做了很多RedGreenRedGreenRedGreen(或更常见的是RedRedRedGreenGreenGreen),但是我很少重构。我的测试当然从未被重构过,因为我一直觉得不编码会浪费更多的时间。但是我可以看到这可能是我现在面临的问题的原因。是时候让我认真考虑进行一些重构和合并了。
奈柔2011年

1
Google C ++模拟框架(与google C ++ test fw集成)-非常非常强大的模拟库-灵活,功能丰富-与那里的任何其他模拟框架都相当。
ratkok 2011年

9

非常有趣的问题。

需要注意的重要一点是C ++并不是很容易测试,而游戏通常也是TDD的非常糟糕的选择。您无法测试OpenGL / DirectX是否使用驱动程序X绘制三角形,而使用驱动程序Y绘制黄色。如果凹凸贴图法线向量在着色器变换后未翻转。您也无法测试具有不同精度的驱动程序版本的裁剪问题,等等。由于调用不正确而导致的未定义绘图行为也只能使用准确的代码检查和手边的SDK进行测试。声音也是不好的选择。多线程在游戏中同样很重要,但对单元测试几乎没有用。所以很难。

基本上,游戏是很多GUI,声音和线程。即使使用可以发送WM_的标准组件的GUI,也很难进行单元测试。

因此,您可以测试的是模型加载类,纹理加载类,矩阵库等,如果代码只是您的第一个项目,那么代码不是很多,并且通常不是很可重用。而且,它们打包成专有格式,因此,除非您发布改装工具等,否则第三者输入的差异不太可能很大。

再说一次,我不是TDD专家或传道者,所以请带些盐。

我可能会针对主要核心组件(例如矩阵库,图像库)编写一些测试。abort()在每个函数中添加大量意外输入。最重要的是,专注于难以破解的抗性/弹性代码。

关于一个错误,聪明地使用C ++,RAII和良好的设计可以有效防止此类错误。

基本上,如果您想发布游戏,则要做很多事情只是为了了解基础知识。我不确定TDD是否会有所帮助。


3
+1我真的很喜欢TDD的概念,并尽可能地使用它,但是您提出了一个非常重要的观点,TDD的倡导者对此保持沉默。正如您所指出的,有很多类型的编程,如果不是不可能的话,编写有意义的单元测试非常困难。在有意义的地方使用TDD,但可以通过其他方式更好地开发和测试某些类型的代码。
Mark Heath

@Mark:是的,如今似乎没人在乎集成测试,以为他们拥有一个自动化测试套件,将所有东西组合在一起并使用真实数据进行测试时,一切都会神奇地工作。
gbjbaanb

同意这一点。感谢您提供一个务实的答案,该答案并不能将TDD规定为所有问题的答案,而不是所有问题的答案,这只是开发人员工具包中的另一个工具。
jb

6

我同意其他答案,但我还要补充一个非常重要的观点:重构成本!!

使用编写良好的单元测试,您可以安全地重新编写代码。首先,编写良好的单元测试可以很好地说明您的代码意图。其次,重构的任何不幸副作用将在现有测试套件中被发现。因此,您已保证旧代码的假设也适用于新代码。


4

其他人如何在不破坏所有生产力和动力的情况下在TDD中生存?

这与我的经历完全不同。您要么是非常聪明的人,并且编写了没有错误的代码(例如,由于一个错误),或者您没有意识到您的代码中存在阻止程序正常工作的错误,但实际上并没有完成。

TDD是要谦虚地知道您(和我!)犯了错误。

对我来说,编写单元测试的时间比从一开始就用TDD完成的项目减少的调试时间节省的时间多。

如果您没有犯错误,那么TDD对您来说对我来说就不那么重要了!


好吧,您的TDD代码中也有错误;)
编码器

真的!但是,如果正确执行TDD,则它们的确是另一种错误。我想说代码必须100%无缺陷才能完成是不对的。虽然如果有人将错误定义为与单元测试定义的行为的偏差,那么我认为它是没有错误的:)
Tom Tom

3

我只有几句话:

  1. 看来您正在尝试测试所有内容。您可能不应该,只是特定代码/方法的高风险和边缘情况。我非常确定80/20规则适用于此:您花了80%的时间为未覆盖的代码或案例的最后20%编写测试。

  2. 优先排序。进行敏捷软件开发,并列出您一个月后要发布的真正需求。然后像这样释放。这会让您考虑功能的优先级。是的,如果您的角色可以进行后空翻,那很酷,但是它具有商业价值吗?

TDD很好,但前提是您不希望100%的测试覆盖率都没有达到目标,并且TDD不能阻止您产生实际的业务价值(即功能,为游戏增添价值的东西)。


1

是的,编写测试和代码可能比编写代码要花费更长的时间-但是编写代码和相关的单元测试(使用TDD)比编写代码然后进行调试更可预测。

使用TDD时几乎消除了调试,这可以使所有开发过程更加可预测,并且最终可以说更快。

持续重构-如果没有全面的单元测试套件,就不可能进行任何认真的重构。建立基于单元测试的安全网的最有效方法是在TDD期间。重构良好的代码可以显着提高维护代码的设计人员/团队的整体生产力。


0

考虑缩小您的游戏范围,并将其放到有人可以玩的地方或您将其释放。保持测试标准而不必等待太久才能发布游戏,这可能是保持动力的中间原因。用户的反馈可能会带来长期的好处,而您的测试可以使您对增加和更改感到满意。

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.