如何设计易于单元测试的游戏软件?


25

在游戏开发环境中使用像JUnit这样的测试框架是否可行?为了使游戏更具可测试性,您可以遵循哪些设计注意事项?可以/应该测试游戏的哪些部分,应该/必须将哪些部分留给人工测试?

例如,如果将游戏循环封装在一个函数中,似乎很难进行测试。我喜欢重构一个“更新”功能,该功能需要时间增量并向前推进游戏逻辑。这提供了一些有趣的技巧,例如通过提供虚假的,较慢的时间增量来降低游戏速度的功能。


6
当您有几乎无休止的奴隶劳动大军为您做游戏测试时,为什么要浪费工时编写单元测试?我开玩笑,我开玩笑...;)
Mike Strobel,2010年

1
实际上,您不必开玩笑,这是一个很好的观点!我没有考虑过,但是游戏是让其他人测试的最简单的软件。我猜这在一定程度上抵消了自动游戏测试的难度(单元或其他方式)。
Ricket 2010年

3
单元测试不一定要确保您的应用程序整体上正常运行(即功能测试)。更多有关确保将来的更改不会破坏现有功能。当然,用户可以看到太空船何时上下颠倒,但是当路径算法偏离.001时,效果可能直到游戏进行200小时后才显现出来。在代码发布之前,单元测试就是这种情况。
韦德·威廉姆斯

1
游戏是让用户的最简单的软件,但是玩!= 测试。获得良好的错误报告非常困难。除此之外,跟踪错误在代码中的何处发生,并验证新的更改不会破坏现有的代码,这两项都将从自动化测试中受益匪浅。
优厚

Answers:


25

TDD的宗旨之一是在某些情况下让TDD影响您的设计。您为系统编写了一个测试,然后编写代码以使该测试通过,并使依赖关系尽可能地浅。

对我来说,在单元测试中只有两件事我没有测试:

首先,我不测试视觉元素以及外观。我测试了对象是否在更新后将放置在正确的位置,相机将在其边界范围内剔除对象,并且在移交给图形引擎之前正确执行了转换(至少是在着色器之外完成的转换) ,但是一旦它碰到了图形系统,我就会画线。我不喜欢尝试嘲笑DirectX之类的东西。

其次,我并没有真正测试游戏的主要循环功能。我测试了每个系统在经过合理的增量后都可以正常工作,并且在需要时可以正常工作。然后,我只是在游戏循环中使用正确的增量更新每个系统。实际上,我可以进行测试以显示每个系统都被调用了正确的增量,但是在许多情况下,我发现这种矫kill过正(除非您使用复杂的逻辑来获取增量,否则就不会矫over过正)。


6
第一段是关键。事实是,TDD要求您比没有代码时要更好地设计代码,仅仅是为了进行测试。当然,许多人认为他们是设计方面的专家,但是当然,对自己的技能印象最深的人通常是学习最多的人……
dash-tom-bang 2010年

2
我不同意代码必然因此会更好地设计。我见过很多讨厌的东西,必须将以前干净的接口打碎并破坏封装才能注入可测试的依赖项。
Kylotan

4
我发现这种情况更多地发生在为现有类编写测试而不是相反的情况下。
杰夫

@Kylotan可能更多是设计不当/测试设计不佳的症状。重构并利用模拟进行干净的测试,不要使您的代码陷入更糟糕的状态;这与应该发生的情况相反。
timoxley 2010年

2
封装是一件好事,但更重要的是要了解为什么这是一件好事。如果仅使用它来隐藏依赖项和一个庞大的丑陋界面,那么它就会大声喊叫,这说明您的内幕设计可能不是很好。封装主要不是要隐藏丑陋的代码和依赖关系,主要是要使您的代码更易于理解,并让消费者在尝试推理代码时可以遵循的路径更少。
Martin Odhelius

19

我相信您正在寻找Noel Llopis进行过单元测试:

关于测试整个游戏循环,还有另一种技术可以防止代码中的功能退化。想法是让您的游戏通过重播系统(例如,首先记录玩家的输入,然后在不同的运行中重播它们)来玩。在重放期间,您将为每个帧生成每个对象状态的快照。状态的集合然后可以称为“黄金图像”。重构代码时,您将再次运行重播,并将每个帧的状态与黄金图像状态进行比较。如果不同,则测试失败。

尽管此技术的好处显而易见,但您需要注意以下几点:

  • 您的游戏需要确定性,因此您需要小心一些事情,例如取决于系统时钟,系统/网络事件,不确定性种子的随机数生成器。等等
  • 生成黄金图像后,内容更改可能会导致与黄金图像产生合理差异。无法解决这个问题-更改内容时,您需要重新生成黄金形象。

+1还值得注意的是,这种方法的价值与“黄金形象”的长度直接相关:如果不够长,您很容易错过错误;如果时间太长,您将需要等待很多时间。重要的是,尝试使游戏在这种模式下运行的速度比正常运行时环境下快一个数量级,以节省时间。跳过渲染会有所帮助!
工程师

9

我同意Jeff和jpaver的意见。我还想补充一点,为您的体系结构采用组件模型可以大大提高其可测试性。对于组件模型,每个组件都应该执行单个工作单元,并且应该可以独立测试(或使用有限的模拟对象)。

同样,依赖实体的游戏其他部分也应仅依赖组件的子集才能发挥作用。实际上,这意味着您通常可以模拟出一些虚假的组件以方便测试这些区域,也可以仅出于测试目的而组成部分实体。例如,您省去了渲染和输入组件,因为不需要测试物理。

最后,我会忽略您从游戏社区中的一些人那里获得的有关界面的性能建议。使用接口编写好代码,如果您遇到性能问题,可以轻松地分析,识别和重构以解决问题。Noel对接口对性能的影响或它们所增加的复杂性开销的担忧并没有使我信服。

就是说,不要过度尝试独立地测试每件事。像大多数事情一样,测试和设计也要达到适当的平衡。


请注意,尽管您似乎对SX并不陌生,但答案并不确定,说“以上两个评论”之类的东西势必会很快被淘汰,因为目前您的投票权是一票制目前其他两个答案中的一个。:)
Ricket

谢谢,我在评论时没有考虑到这一点。此后,我已经更新了评论以挑选单个评论。
亚历克斯·谢尔

3

我以为我将添加第二个答案,以回应OP的评论,即用户可以替换单元测试。我相信这是完全错误的,因为单元测试的目的不是确保质量。如果要进行适当的测试以确保程序的质量,则应该调查方案测试或进行大量监视。

(通过出色的监控和产品作为服务运行,确实可以对客户进行一些“测试”,但是您需要拥有一个复杂的系统,该系统可以快速检测到错误并回滚负责的更改。对于一个小型开发团队。)

单元测试的主要好处是,它们迫使开发人员编写更好的设计代码。测试行为对开发人员提出了挑战,要求他们将关注点分离并封装数据。它还鼓励开发人员使用接口和其他抽象,以便他仅测试一件事。


同意,除了单元测试不会强迫开发人员编写好的代码。还有另一种选择:写得不好的测试。您需要编码人员对单元测试的想法做出承诺,以防止他们感到厌倦。
tenpn

2
当然可以,但这没有理由把婴儿和洗澡水一起扔出去。负责任地使用工具,但首先使用工具。
亚历克斯·谢尔
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.