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

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

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

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

第50个测试涵盖了游戏中的特定实体及其创建和使用的各个方面,我查看了我的待办事项清单,看到了下一个要编码的实体,并为编写另外50个类似的测试而感到震惊。实现它。

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

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

评论

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

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

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

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

我要反对流行观点,并说,如果您要开发游戏,那么TDD不一定总是正确的选择。由于gamedev.stackexchange上的某个人已经很好地回答了这个问题,因此我将其链接到此处。

#1 楼

首先,我要感谢您分享您的经验并表达您的疑虑……我不得不说这并不少见。


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

这使我进入最终查询:如何做我会好起来吗?我的答案是阅读,反思和练习。

例如最近,我一直在关注


我的节奏是否反映了RG [Ref] RG [Ref] RG [Ref]还是RRRGRGRef。
% /编译错误状态。
我陷入了红色/构建失败状态吗?


评论


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

–内鲁
2011年6月22日12:25

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

–吉寿
2011年6月22日13:39

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

–内鲁
2011年6月23日在2:20

@asgeo-无法编辑该评论。.该链接已选择了结尾的方括号。这应该有效-goo.gl/dWp3k

–吉寿
2011年6月24日,下午2:34

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

–lurscher
11年6月24日在21:14

#2 楼

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

评论


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

–克里斯托弗·马汉(Christopher Mahan)
11年6月24日在23:32

即使100%的测试覆盖率,您也没有100%的信心。那是测试101。测试不能证明该代码没有缺陷。相反,他们只能证明它确实包含缺陷。

– CesarGon
2011年6月25日,0:40

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

–机会
2011年7月4日在20:26



另外(至少我上次检查工具时),代码覆盖率报告与行是否执行有关,但不包括值覆盖率,例如今天,我有一个错误报告说,我可以在测试中执行代码的所有路径,但是由于存在a = x + y这样的行,尽管代码中的所有行都在测试中执行,所以测试仅针对以下情况进行测试: y = 0,所以该错误(应该是a = x-y)在测试中从未发现。

– Pete Kirkham
16-2-26在12:45



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

– Darius.V
17年3月21日在19:25

#3 楼


TDD仍然使我的速度大大降低


这实际上是错误的。

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

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

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

评论


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

– Peter Long
2011年6月22日在2:09

@Peter Long:代码质量。今年是“测试”年,我们最终选择了最有效的废话软件。

– S.Lott
2011年6月22日,下午2:10

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

–马克·托马斯(Mark Thomas)
2011年6月22日,下午2:12

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

–数学家
2011年6月22日下午5:20

@Peter Long:“经过的时间是相同的” ...在此期间的任何时候,您都可以交付有效的代码。

–坦率的剪毛
11年6月24日在16:34

#4 楼


或者,在完成第50次测试以涵盖游戏中的特定实体及其创建和使用的各个方面之后,我查看了我的待办事项清单,并查看了要编码的下一个实体,并惊恐于想编写另外50个类似的测试以使其实现的想法。 >当所有测试通过时,就该重构代码并删除重复项了。人们通常会记住这一点,但有时他们会忘记这也是重构测试,删除重复项和简化事情的时候。

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

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


尽管您有写测试的丰富经验,但您仍然不知道如何编写测试,那么那绝对是代码的味道。您的代码某种程度上缺乏模块化,这使得编写小型简单测试变得困难。

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

编辑:关于单元测试的哲学,我认为您可能会很感兴趣地读到:Testivus的方式

更实用,如果不一定很有帮助,请指向:


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


评论


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

–斯科特·惠特洛克
2011年6月24日,1:17



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

–贾斯汀
2011年6月24日在1:56



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

– anon
2011年6月24日在2:58



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

–内鲁
2011年6月25日16:25

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

–ratkok
2011年6月25日18:25

#5 楼

一个非常有趣的问题。

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

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

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

再说一次,我不是TDD专家或传播者,所以

我可能会写一些主要核心组件的测试(例如矩阵库,图像库)。在每个函数的意外输入中添加一堆abort()。而且最重要的是,专注于不会轻易破坏的抗性/弹性代码。

关于一个错误,聪明地使用C ++,RAII和良好的设计可以有效地防止它们发生。 br />
基本上,如果您想发布游戏,则要做很多事情只是介绍基础知识。我不确定TDD是否会有所帮助。

评论


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

–马克·希思(Mark Heath)
2011年7月8日在9:58

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

– gbjbaanb
11年8月8日在10:24

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

– j b
19 Mar 3 '19 at 10:43

#6 楼

我同意其他答案,但是我还想补充一点很重要的一点:重构成本!!

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

#7 楼


别人如何在TDD中生存而又不牺牲所有生产力和动力?


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

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

对我而言,编写单元测试的时间比从一开始就使用TDD完成的项目所花费的调试时间减少的时间多得多。

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

评论


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

–编码器
2011年6月28日13:53

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

–汤姆
2011年7月4日在20:02



#8 楼

我只说几句:


您似乎正在尝试测试所有内容。您可能不应该,只是特定代码/方法的高风险和边缘情况。我很确定80/20规则适用于此:您花了80%的时间为未覆盖的代码或案例的最后20%编写测试。
确定优先级。进行敏捷软件开发,并列出您一个月后要发布的真正需求。然后像这样释放。这会让您考虑功能的优先级。是的,如果您的角色可以进行后空翻,这很酷,但是它具有商业价值吗?

TDD很好,但前提是您不打算100%覆盖测试,并且不是这样会使您无法产生实际的业务价值(即功能,为游戏增添些东西的东西)。

#9 楼

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

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

恒定重构-不可能做任何严肃的事情无需全面的单元测试套件即可进行重构。建立基于单元测试的安全网的最有效方法是在TDD期间。重构良好的代码可以显着提高维护代码的设计人员/团队的整体生产力。

#10 楼

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