在游戏开发中,测试驱动开发是一种常规的方法吗?

33
我只是好奇,因为我看到的所有TDD示例都与Web编程有关。如果这不是一种常规方法,那为什么不呢?

请问您能否在问题中加入“测试驱动开发”这个完整名称?我不确定每个人都知道它是什么,而且像这样简短的问题如果您不知道缩写的话就无法理解。 - Lena Schimmel
如果你知道什么是测试驱动开发,我猜你也知道TDD是什么,但为了避免混淆,我会更改标题。 - terjetyl
8个回答

57

TDD已成为认真对待自己职业的软件开发人员喜爱的方法。[IEEE:TDD]该方法的好处显著,成本相对较低。[The Three Laws of TDD]

没有哪个软件领域是不适合或无效的TDD。然而,有些领域确实具有挑战性。游戏恰恰就是其中之一。

事实上,挑战并不是游戏本身,而是UI。之所以UI具有挑战性,是因为你通常不知道想要UI看起来像什么,直到你看到它。UI是那种必须要摸索出来的东西。正确地完成它是一个深度迭代的过程,充满了曲折、死胡同和小巷。首先为UI编写测试可能会很困难,也会浪费时间。

现在,在所有人都会说:“Bob叔叔说:‘不要为UI做TDD’”之前,让我说一下。难以为UI做纯TDD并不意味着你不能为几乎其他所有东西做纯TDD。很多游戏都是关于算法的,你可以使用TDD来编写这些算法。尤其是在游戏中,有一些算法代码就像UI一样需要调整,因此可能无法首先进行测试。但是还有许多其他算法代码应该首先进行编写测试。
关键在于遵循单一职责原则(SRP),将那些需要调整的代码与那些确定性代码分开。不要将易于测试的算法与UI混合在一起。不要将推测性代码与非推测性代码混合在一起。将因A原因而变化的事物与因B原因而变化的事物保持分离。
此外,请记住:某些代码难以首先测试,并不意味着这些代码难以第二次测试。一旦你已经调整好了代码并使其按照你想要的方式工作,然后你可以编写测试来证明代码按照你的想法工作。(你会惊讶地发现在这个过程中有多少次发现了错误。)
写完代码再写测试的问题在于,通常代码之间耦合度太高,很难编写最有帮助的精细测试。因此,如果你正在编写难以先行测试的代码,应该遵循依赖倒置原则(DIP)开闭原则(OCP),以保持代码解耦,方便事后测试。

8
这篇文章清晰而准确地指出了TDD方法及其倡导者存在的问题。它并不被视为一个有助于解决软件开发中一些问题的有用工具,而是一套无所不在的食谱式“原则”,必须无论何时何地都要遵循。 - Rune Braathen
2
同意Rune的观点,尽管我认为我们都可以从UB的SOLID原则中学到很多东西,但他及其追随者传达教义的方式常常让人感到不灵活。 - Echostorm
1
我喜欢称之为模拟船仓税。这些是设计上的教条主义方法。实际上,TDD 的所有内容都是关于紧密反馈循环以帮助设计系统。仅仅因为“违反” SRP 就教条主义地将事物拆分开来是短视的。OCP 只是另一个安全保障,我的测试套件允许我快速更改并知道是否有任何问题,而无需使用继承/组合。DI 是为我们处理静态类型语言的人准备的。甚至被误用了。为什么我们要进行提取接口的仪式呢?我们可以 DI 一个委托使其可测试。 - Amir
2
"TDD已成为认真对待自己职业的软件开发人员所青睐的方法。" 谬论。五年后,Bob大叔改变了他对TDD和职业主义的看法。我认为TDD已经成为那些以教授TDD为生的人们所青睐的方法。 - Pablo Lascano
4
如果你认为你所分享的文章表明了Uncle Bob改变了他的想法,我认为你需要重新阅读一下它…… "我不相信TDD是专业主义的前提条件......它目前在专业行为中发挥着重要作用......随着我们展望未来,它将发挥更加重要的作用。" 然后,他将不使用TDD的程序员比作不洗手的医生,并以“今天,在我们历史上的这个时刻,TDD不是我认为它将成为专业主义的前提条件。”结束。 - Cam Jackson
显示剩余3条评论

20
简单的答案是"不是",TDD在游戏开发中不是一种常见的方法。有些人会指出Highmoon和Neil Llopis作为反例,但游戏行业很大,他们是我所知道的唯一完全采用TDD的人。我相信还有其他人,但这是我知道的唯一的例子(我已经在这个行业工作了5年)。
我认为我们很多人都曾尝试过单元测试,但由于某种原因并没有成功。从个人经验来看,让游戏工作室转向TDD是很困难的。通常情况下,代码库会在项目之间保持不变,将TDD应用于一个巨大的现有代码库既繁琐又麻烦。我相信最终它会证明是有益的,但让游戏程序员接受它是困难的。
我在编写低级游戏引擎代码的单元测试方面取得了一些成功,因为这些代码往往具有非常少的依赖关系并且易于封装。然而,这始终是事后测试而不是TDD。高级游戏代码通常更难编写测试,因为它具有更多的依赖关系并且通常与复杂的数据和状态相关联。以AI为例,要测试AI需要某种上下文环境,例如导航网格和世界中的其他对象。在隔离中设置这样的测试可能是不容易的,特别是如果涉及的系统没有为此设计。
更常见的做法是烟雾测试,我个人也取得了更多成功。你经常会看到烟雾测试与持续集成一起使用,以提供关于代码行为的各种反馈。烟雾测试更容易,因为只需将数据输入游戏并读回信息即可,而无需将代码分隔成微小的可测试部分。再以AI为例,你可以告诉游戏加载一个关卡,并提供一个载入AI代理并给它指令的脚本。然后你只需确定代理是否执行了这些指令。这是烟雾测试而不是单元测试,因为你正在运行整个游戏而不是在隔离中测试AI系统。

在我看来,通过对低级代码进行单元测试和对高级行为进行烟雾测试,可以获得不错的测试覆盖率。我认为(希望)其他工作室也采取了类似的方法。

如果我的TDD观点听起来有些模棱两可,那是因为确实如此。我对其仍然持有一定的保留意见。虽然我看到了一些好处(回归测试,强调先设计后编码),但在与现有代码库合作时应用并强制执行它似乎会导致许多麻烦。


1
我认为你的意思是系统测试(“对完整、集成的系统进行的测试”),而不是烟雾测试(“在组装或修复系统后进行的第一次测试,以提供一些保证被测试的系统不会发生灾难性故障”)。 (引用自各自的维基百科文章。) - futlib

10

Games from Within发布了一篇文章,讨论了他们使用单元测试的情况、单元测试在游戏中的局限性以及他们建立的自动化功能测试服务器,以帮助解决这个问题。


6
如果您提到为每个代码编写和维护单元测试的实践,我猜测这在游戏行业中并不普遍。有很多原因,但我可以想到3个明显的原因:
1. 文化方面:程序员本来就是保守的,游戏程序员更是如此。 2. 实际应用方面:TDD不太适合问题域(移动部件太多)。 3. 时间压力方面:永远没有足够的时间。
TDD最适用于应用程序领域,该领域不太具有状态性质,或者至少其中的移动部件不会同时运动,随便说一下。
TDD可应用于游戏开发过程的某些部分(基础库等),但在这个职业线上,“测试”通常意味着运行自动飞行、随机按键测试、计时io负载、跟踪fps峰值,确保玩家不能通过扭曲方式导致视觉不稳定等内容。机器人也经常是类似于人形的。
TDD可以是一个有用的工具,但它作为一个必须普及的银弹的地位是值得质疑的。开发不应该被测试驱动,而应该是理性的。虽然RDD是一个糟糕的缩写,但它不会流行。 ;)

4
可能主要原因是TDD更适合那些语言,但除此之外,游戏本身与这种范式并不搭配。一般来说(我确实是指一般性的讲法,所以请不要用反例淹没我),测试驱动设计最适合事件驱动系统,而对于模拟式系统则不太适用。你仍然可以在游戏的低级组件上使用测试,无论是测试驱动还是普通的单元测试,但对于更高级的任务,很少有任何离散事件可以模拟出确定性结果。
例如,Web应用程序通常具有非常明显的输入(HTTP请求),会更改非常少量的状态(例如数据库中的记录),并生成大部分确定性输出(例如HTML页面)。这些可以很容易地检查其有效性,并且由于生成输入很简单,所以创建测试是微不足道的。
但是,对于游戏而言,输入可能难以模拟(特别是如果需要在某个时间点发生...想想如何通过加载屏幕、菜单屏幕等),更改的状态量可能很大(例如,如果您有一个物理系统或复杂的反应AI),而输出很少是确定性的(随机数使用是主要问题,虽然浮点精度损失是另一个问题,硬件规格、可用CPU时间、后台线程的性能等也可能是问题)。
要进行TDD,您需要确切地知道在某个事件中您期望看到什么,并且需要有一种准确的测量方法,而这两个问题对于模拟避免离散事件、有意包含随机因素、在不同机器上表现不同、具有图形和音频等类比输出的仿真来说都很困难。
此外,还存在一个巨大的实际问题,即进程启动时间。您将想要测试的许多内容都需要加载大量数据,如果您模拟数据,则无法真正测试算法。考虑到这一点,拥有仅执行单个任务的任何类型的测试脚手架很快就变得不切实际了。您可以在无需每次关闭Web服务器的情况下对Web服务器运行测试-这在游戏中很少可能,除非您从嵌入式脚本语言(这是合理的,确实在业界发生)中进行测试。
例如,您想将体积阴影渲染添加到游戏内的建筑物中。因此,您需要编写一个测试,启动所有必要的子系统(例如渲染器、游戏、资源加载器),加载建筑物(包括网格、材质/纹理),加载相机,将相机定位到指向建筑物的位置,启用阴影,渲染场景,然后以某种方式决定阴影是否实际出现在帧缓冲区中。这是不太实用的。实际上,您已经有了所有这些脚手架,形式为游戏本身,并且除了代码本身内部的任何断言之外,您只需启动它来进行视觉测试。

1

大多数游戏开发者在现代开发实践方面并不是很精通。谢天谢地。

但测试驱动的开发模型强调首先集中精力考虑如何使用某个功能,然后再详细说明它的作用。总的来说,这样做是有好处的,因为它迫使你集中精力考虑一个特定的功能实际上将如何适应你正在做的任何事情(比如一个游戏)。

所以优秀的游戏开发者自然而然地做到了这一点。只是没有明确表达出来。


1

@Rune 再次强调,请将重点放在“D”而非“T”上。在单元层面上,测试是一个思考工具,可以帮助您理解自己想要什么并推动代码的设计。在单元层面上,我发现最终得到的代码更加清洁、稳健。我把高质量的代码片段放入系统中,它们之间的匹配度就越好,产生的错误就越少(但不是没有)。

这与游戏需要进行的严格测试完全不同。


我真的看不出这与我所写的有任何区别。在编写代码时考虑可测试性总是很好的,但它只能帮助您解决相对较大规模游戏的一部分测试需求。 - Rune Braathen

0

测试驱动开发(TDD)在任何地方都不是一种“正常”的方法,因为它仍然相对较新,并且尚未被普遍理解或接受。这并不意味着现在没有一些公司采用这种方式工作,但我仍然很惊讶听到有人在这个时候使用它。


我不会点踩或做任何事情,但TDD被谈论得如此之多(无论它是否真正有价值),以至于我很惊讶你“在这一点上仍然惊讶听到有人使用它”。 - Mark Rogers
TDD 在航空航天或医学影像等行业已经成为标准至少十年。在它流行之前,可能没有被称为 TDD,但自动化回归测试、代码覆盖率等都早已存在。 - Kena
不是挑剔,但自动化测试并不一定意味着有人在进行TDD。 - Dunk
m4bwav - 人们谈论很多事情,并不意味着他们真正理解或实施了它们。Kena有点证明了我的观点。Kena- 回归测试和代码覆盖率并不等于测试驱动开发(TDD)。 - Echostorm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接