游戏开发的测试






4.57/5 (5投票s)
本文涵盖了理论、原则和实用技术,以帮助您的测试工作。
首次发表于 What Could Possibly Go Wrong。
关于测试,最重要的一条规则就是去做。
Kerningham Pike 1999
你开发游戏吗?是?那这篇文章就是为你准备的。无论你使用什么语言、引擎、API或平台,都无所谓。无论你制作了多少游戏,计划制作多少游戏,甚至你的游戏规模有多大,都无所谓。这篇文章仍然适合你。因为无论你创造什么游戏,它都需要测试。你总不会把自己做的东西不亲自试一下就交出去吧?
把代码胡乱拼凑起来就发布出去的牛仔式编程,并不能算是最佳实践。我们把修改代码和追逐错误的循环称为打地鼠式除错(bug whack-a-mole)。这个浪费资源的过程会导致程序员压力陡增,决策质量越来越差。
你需要知道,你刚刚写的代码在提交到版本控制系统之前,很可能是可以工作的。更重要的是,你需要知道你没有破坏掉其他已经测试过的东西。
在本文中,我们将涵盖:
- 我们的测试理念。
- 通过测试来指导开发,而不是更常见的测试跟随开发的方法。
- 理解为什么测试驱动开发很重要。
- 用于测试的流程、技术和工具。
- 简要介绍在Unity引擎中的测试,因为我们在Real Serious Games使用它。
本文主要面向程序员,尽管其他领域的专业人士也能从中受益。我们力求内容尽可能广泛,这里的知识来源于游戏行业多年的经验(以及相关的痛苦)。我们相信,任何水平的开发者都能从中有所收获。
给QA的说明
本文更多地是关于作为开发过程一部分的测试,而不是质量保证。我们在此所说的任何内容都不会削弱一个优秀QA部门的重要性。我们相信,一个优秀的QA部门应该与开发团队紧密合作,深度整合,并积极参与产品的构建。
目录
目录 由 DocToc 生成
成果
测试能带给你什么?这个问题似乎不难回答。测试验证了你的软件能正常工作,对吗?
是的。但我们可以更深入地探讨,并且可以更具体。测试验证了你的软件能按预期工作。更重要的是,它验证了你的软件不会出现意想不到的后果(例如格式化你的硬盘,当然,除非它本身就是个格式化程序!)。
这是我们进行测试的主要原因,然而,还有许多其他的附带效应,可以改善我们的流程、产品,并帮助我们保持理智。让我们来看看其中的一些。
测试帮助我们理解我们的软件实际上是如何工作的。这一点怎么强调都不过分。你是否曾回头看过你3个月前写的代码?你还理解它是如何工作的吗?再加上一个开发者团队、沟通问题、复杂的需求、不断变化的范围和演进的设计。3个月后,你还理解它是如何工作的吗?大型复杂的软件会表现出涌现行为,并且永远不会像我们希望的那样可预测。这一点在游戏中尤其如此。
结合复杂的游戏逻辑和不可预测的玩家行为,你是否有信心你的游戏能在每一种情况下都优雅地应对?测试能产生对产品的知识和理解。这减少了不确定性,从而降低了产品风险。我们不仅在谈论游戏发布时带有隐藏问题的风险,尽管我们已经看到这个问题甚至影响到最大的3A级游戏。我们也在谈论构建错误东西的风险,这是一个在软件开发中非常普遍的问题。当然,这在游戏开发中可能更难确定,但只有通过玩和测试你的游戏,你才能更好地理解产品是什么以及它应该走向何方。
测试在很大程度上可以自动化。这需要相当多的时间和精力,并且永远不能完全取代手动测试,但它可以消除大部分重复性测试。不可否认,实现自动化测试可能很困难。但当你做到时,它就成了支撑你项目稳固的脚手架,以及让项目在不断变化和持续演进中保持稳定前进的轨道。没有自动化测试,要让软件长期保持正常工作会困难得多——尤其是在多人参与和快速推进的情况下。
测试可以改进规划和设计。在编码前所做的任何思考工作都能极大地提高第一次就做对的几率(或者至少更接近目标)。有一个测试计划能帮助你了解你的前进方向。这有助于你摒弃那些耗时但最终不为产品增加价值的忙碌工作。尽早并迅速地削减不必要的范围是你能做的最有效的生产力提升之一。因为你经过了深思熟虑并将其阐述为测试计划,从而拥有了一个明确的概念,这有助于巩固和完善你的愿景。这种远见可以帮助你识别愿景中的缺陷,并帮助你拒绝那些你认为行不通的想法。在可能的情况下,你应该在为坏主意付出血汗之前尽早拒绝它们。
经过良好测试的软件的特性
在我们深入探讨测试的理念和技术之前,我们应该首先看看经过良好测试的软件所应具备的特性。正是为了满足这些特性,我们才开始了我们的测试之旅。
完整性
代码是否完成了它所有应该做的事情?
正确性
代码工作时是否没有错误?
性能和资源使用
代码是否具有可接受的性能和资源使用率?
可靠性/稳定性
代码工作是否可靠?代码能否优雅地处理意外事件和错误数据?
趣味性(仅限游戏的额外品质)
在满足以上所有条件的同时,游戏是否仍然有趣?
现在,要勾选的框很多,但我们的目标并不是在所有方面都拿到满分。软件开发的很大一部分在于权衡、判断和平衡,以完成工作。在许多情况下,差不多好就必须足够好,才能过关。
作为游戏开发者,最后一个特性常常是我们脑海中最核心的部分。虽然我们这里讨论的大部分技术对于测试游戏的趣味性品质毫无用处(将其交到玩家手中始终是最好的选择),但我们可以保证,不满足其他任何一个品质都将对趣味性产生负面影响,因为一个因错误或性能差而无法玩的游戏,根据定义就是不好玩的。
理念
通过测试来指导开发过程(又称测试驱动开发或TDD)是我们理念的一个主要部分,但我们不只是在谈论单元测试。在编码前进行的任何思考、计划或参与测试活动的努力,都会对开发产生积极影响,我们认为这也是一种测试驱动开发的形式,尽管它可能更接近于其他开发者通常认为的行为驱动开发。
你第一次接触TDD很可能是在以测试先行的方式编写单元测试时。我们从TDD和单元测试开始,尽管后来我们意识到,TDD也可以在没有单元测试和任何形式的自动化的情况下应用(尽管自动化测试非常有益,并且通常是TDD的最终目标)。
我们旨在结果导向。结果是什么?我们的目标是什么?我们如何衡量成功?这个结果带来的价值是否超过其开发成本?这些是你应该回答的问题。随着你对项目的理解加深,你很可能会逐步找到答案。测试驱动开发需要你理解你的方向并专注于结果。你必须知道你的方向,并能在开始前进行规划。如果你不知道,那就花点时间,快速制作一个原型来测试你的想法,然后再回到测试驱动的流程中来。
我们旨在拥有质量思维。这意味着我们的目标是高质量的成果。我们如何定义质量?这个问题无法在这里详述,它对不同的人、在不同的情况下、对于不同的项目意味着不同的东西。然而,我们提倡一种缺陷优先的开发过程。也就是说,在可能的情况下,我们倾向于在添加新功能之前修复错误。我们正面处理https://en.wikipedia.org/wiki/Technical_debt)">技术债务,以在项目生命周期内保持稳定性。不断累积的缺陷等同于项目无法按时交付的风险不断增加。最小化缺陷和技术债务有助于我们保持快速的变更节奏和低成本的实验。测试有助于建立一个以质量为中心的思维模式,并促进高质量的产品。这可能是在说显而易见的事情,但必须指出,构建一个按预期工作、意外问题最少的高质量产品,是测试的目标。
我们的理念意味着我们试图预先定义我们的方向和成果。当然,我们并不总能做到正确,但至少我们始终知道我们应该走向何方。我们必须保持警惕,并不断监控我们的航向。我们必须准备好改变航向,以保持竞争力,并能够打造出最好的游戏。有时,我们必须退后一步,重新评估和重新规划。
那么,测试驱动开发如何帮助我们实现经过良好测试的软件的特性呢?通过我们的理念和流程,我们可以...
定义可接受的标准
每个特性的可接受质量水平是什么?这取决于目标硬件、受众和游戏类型。测试驱动开发迫使你提前计划。毕竟,你必须知道要测试什么。
识别我们何时未达到可接受的标准
每当添加或修改一个功能时,你都可以通过查看测试输出来了解其影响。当出现问题时,我们有记录显示添加了什么以及它如何影响了游戏。
估算计划外范围变更的影响
你在会议中,有人提出了一个新想法。所有人都把头转向你,一个声音响起:我们能做到吗?幸运的是,你手头有最新的指标,可以给出一个相当有根据的猜测作为回应。
我们最重要的两个原则是保持其正常工作和测试不是一个阶段。对自己重复这两句话。把它们贴在显示器旁边的墙上。如果在推动项目前进的过程中,无论是以你满意的速度还是别人要求的速度,你无法遵守这两条准则,那就意味着你正在失去对流程的控制。你将失去保持快速步伐的能力,估算任务的能力也会减弱。
最终,关于测试驱动开发的主要启示是,编码前进行规划总是一个好主意。
自动化
自动化在游戏开发中非常有用。它能将漫长、繁琐且易错的流程变得快速、可重复、可靠,并有可能解放你的时间去做真正重要的事情。这些额外的时间可以帮助你专注于制作更好的游戏。
然而,构建自动化是昂贵且耗时的。从长远来看,它物有所值,但你必须确保它不会过多地分散你实际制作游戏的注意力。毕竟,你必须完成那个游戏,以确保你能在长期中生存下来。
你需要权衡在自动化上投入多少时间。好处是显而易见的,但成本可能很高。自动化的成本应该在你的项目生命周期内分摊。以时间盒(time-boxed)的方式逐步发展你的自动化系统,这将防止你在上面花费太多时间。随着时间的推移,你将建立起你的技术和工具箱,这些可以沿用到下一个项目,以及再下一个项目。正如我们所说,这是一项长期投资。随着时间的推移,自动化需要的工作越来越少,当你的项目由自动化测试框架保驾护航时,其收益最终将大大超过成本。一旦有人提交了破坏你的构建或现有功能的更改,你就会收到通知。这样的系统使得管理技术债务变得容易得多。
你该如何开始?从自动化你的构建过程和创建一个持续集成系统开始(稍后会详细介绍)。然后可以自动化其他你做3次或更多次的任务。不要太担心这是如何工作的……只要确保每个任务都值得自动化,并且80%或更多的工作量是自动化的。这将为你节省大量时间。记住,自动化可能很昂贵,自动化某件事所花费的时间应该远远超过手动完成该任务的成本,如果不是,那么自动化就不值得。
逐步开始编写自动化测试。单元测试需要大量的技巧、工作和细心,并对代码设计有重大影响(是好的影响,但要做好需要时间)。考虑将单元测试用于你最重要、最困难或最麻烦的代码。集成测试更容易实现,且性价比更高。集成测试对代码设计的影响也较小,这使得为现有代码构建集成测试变得更容易。冒烟测试针对最终构建运行,是目前最简单的,所以值得从它们开始。下面将详细介绍这些测试类型。
进程
当我们开发软件时,我们在一个流程中工作。如果你是开发新手,那么你可能还在创建你的流程。如果你已经从事这个行业一段时间,你可能仍在创建你的流程。我们这么说是因为软件开发过程是一个不断演变的系统。它必须改变以适应公司、团队和项目的变化需求。要时刻留意对流程的改进,包括放弃那些不再提供价值的部分。
测试必须融入我们的开发流程。它不应被视为一个独立的阶段或另一个部门的责任。测试是开发的内在组成部分,并与之密不可分。请记住,测试不是一个阶段,它与开发过程紧密相连。测试发生在开发之前、之中和之后。整个过程应该是迭代的、敏捷的,并且上一轮迭代的反馈会影响当前迭代,以此类推。
测试驱动开发的一般流程是设定期望,编写代码并添加产品功能,然后验证代码和功能是否满足最初设定的期望。当使用单元测试进行TDD时,设定期望部分涉及编写实际的代码。当然,TDD的替代方案是传统的开发方法:编写产品代码,手动测试它是否正常工作,重复此过程。
TDD的关键在于理解你的产出并在编码前设定期望。如果你正在编写自动化测试,那么你的期望将以代码的形式表达。如果你因为任何原因没有这样做,就用英文写下你的期望:这通常被称为测试计划。无论你是编写自动化测试还是仅仅在纸上写下你的期望,都不那么重要,重点是你理解你想要实现的目标,并且在代码完成时有一个可以验证的计划。当然,TDD结合单元测试和自动化意味着你可以继续在未来自动运行我们的测试,从而确保你的代码永远不会被意外破坏,这种情况在没有自动化的情况下似乎经常发生,并且随着更多开发者加入团队以及代码为满足产品不断变化的需求而快速演进时变得更糟。
在我们看单个迭代的流程之前,有一个提醒:不同的公司和团队有不同的需求,所以不要害怕接受这个流程,改变它,让它为你工作。当你需要开辟自己的道路时,不要害怕抛弃现有的系统。取其精华,构建你自己的流程,完美地适应你自己的需求。
当你阅读这部分内容时,请不要假设我们所说的迭代会是一个完美分割的瀑布模型。软件开发是一项混乱而复杂的工作,很少会这么简单,但我们确实需要展示测试如何融入这个过程,所以让我们来看看。
话虽如此,一个典型的开发迭代可以分解为以下步骤:
1. 原型制作
可选。如果你已经足够了解自己要做什么,可以开始计划,那么这一步就不是必需的。为了弄清楚你的方向,你应该制作原型。这里的关键是最小化投入。知道你需要回答的重要问题是什么。只制作足以回答这些问题的原型。有意识地快速工作,走捷径,不要担心测试。确保这项工作是有时间限制的。不要打磨这项工作。那些经过打磨并且差不多能用的原型,常常最终会进入生产环境。有时这样做没问题,但其他时候,原型粗糙的做工会在项目结束前给你带来多次麻烦。
2. 产品规划与设计
规划你产品的下一个增量。如果你正在做Scrum,这就像是冲刺规划。专注于结果。你的主要目标是什么?你将交付什么?它带来什么价值?为什么它很重要?
代码设计也可以是其中的一部分,但不要在这里过于关注。我们提倡通过开发来进行设计,也就是说,技术规划和代码设计,就像测试一样,应该在编码前直接进行。我们觉得这些活动是开发过程中内在的、持续的一部分。
3. 测试规划
规划你将如何测试这个增量的结果。规划测试所需的工具。你已经有这些工具了吗?你是否有开发所需工具的计划?
4. 自动化
花一些专门的时间来构建自动化测试流程的工具。同时,也要规划学习和提升你的测试驱动开发技能的时间。为这些活动设置时间限制(Time-box)。每次迭代花少量时间来改善你的开发基础设施,日积月累,将为未来的迭代带来回报。将构建和改善开发基础设施的成本在项目生命周期内分摊是值得的。这相当昂贵,如果一次性完成可能需要数月的开发时间,所以要一点一点地构建你的基础设施。
5. 开发
开发由以下深度交织的活动组成:设计、编码和测试。将它们分开考虑感觉不对。迭代的开发部分,当然是主要部分,是设计、技术规划、编码和测试的日常循环。这个过程会重复进行,直到迭代的目标达成。在这个阶段,有一个紧密的反馈循环非常重要。测试驱动开发会让你专注于结果。
6. 探索
在每次迭代中,留出时间来探索产品,寻找问题。很多问题都是这样发现的。我们编码人员有偏见,我们经常为代码应该如何工作的方式或我们预期会发现的问题编写测试。但通常,真正的问题是由玩家做出意想不到的行为引起的。这是你的QA部门会表现出色的一个领域。他们会做你没想到的事,并发现你没预料到的问题。
7. 评审
评审迭代的结果。目标是否达成?游戏是否符合我们测试计划设定的标准?哪些问题阻碍了我们?我们如何在下一次迭代中改进测试?这是重新评估你的计划和方向,并思考能让你在下一次迭代中更有效率和效果的过程改进的时候。如果你在做Scrum,这就是一次冲刺评审。
测试的类型
手动/重复性测试
手动测试是手动遵循测试计划来测试软件缺陷的过程。这不难,也不是自动化的。以这种方式开始既简单又便宜,但随着时间的推移,成本会不断增加,因为你在一轮又一轮的测试中重复完全相同的过程。随着时间的推移,你应该寻求自动化这种形式的测试。
探索性测试
预留时间来探索你的游戏,而不是机械地执行你的测试计划。你会发现一些原本不会发现的问题。
焦点测试
将你的游戏展示给目标受众的样本群体。他们会以你意想不到的方式使用它,并发现你做梦也想不到的错误。
浸泡测试
让你的游戏通宵运行。让你的游戏连续运行几天。拥有某种程度的自动化确实有助于使这成为可能。你可能需要能够编写一个游戏通关脚本,或者使用演示或吸引模式(如果你有的话)。确保你输出了帧率和内存使用情况的日志。将性能指标图表化可以让你检查性能是否在测试期间变得越来越差。
自动化测试的类型
单元测试
代码单元在隔离环境中进行测试。依赖注入和模拟(mocking)技术模拟了与其他代码模块的连接。单元测试的构建成本很高,尽管随着更好的工具和更多经验的积累,成本会降低。单元测试至少对于你最困难和复杂的代码,即那些需要坚如磐石般可靠的代码来说是值得的。为单元测试而设计代码会产生非常松散耦合的代码,所以以这种方式工作会对你的系统设计产生积极影响。
集成测试
这与单元测试类似,但通常是多个代码模块或系统的相当一部分被作为一个整体一起测试。被测代码的边界比单元测试的要大。单元测试通常测试单个类,而集成测试则与更多协作的类一起工作。
集成测试更像是测试最终的(集成的)系统,因为(通常)系统的任何部分都不会被模拟。它比单元测试的成本要低得多,因为隔离所带来的设计限制被解除了,这意味着你可以尽情地编写紧密耦合的代码。你的代码不必设计得那么好……这可能是你为了快速完成某件事而做出的权衡。
为可抛弃性而设计是我们目前非常感兴趣的一个概念。这是指你设计系统的某些部分(可以想象成组件或微服务),以便最终将其丢弃。整个系统,即各部分的集成,其设计方式使得这些部分可以被丢弃和替换,而对整个系统影响或损害很小。系统继续运行,随着时间的推移,我们替换掉它的整个部分。使用TDD和集成测试来构建你的系统部分。不要太担心部分内部的良好设计,你计划有一天会把它扔掉,所以凑合用是可以的。
冒烟测试
冒烟测试通常测试整个完整的系统(或至少是其某个主要部分)。我们喜欢称之为完整构建测试,因为测试将针对你的整个游戏运行。这是成本最低的测试形式,但开始时可能很困难。你需要能够以某种方式为你的游戏编写脚本或重放输入。你还需要能够确定游戏在测试期间在做什么,幸运的是,如果你已经有良好的日志记录和指标输出,这部分并不难。测试框架可以检查这些输出来验证游戏是否在做它应该做的事情。这种测试可以是广泛的,也可以只是浅尝辄止,任何程度的冒烟测试都将对保持你的游戏长期正常工作产生巨大影响。
技术
测试技术有很多。本节总结了我们认为有用的技术。
玩你的游戏
这似乎很明显,但这是测试的起点。经常玩你的游戏。
测试实际的构建版本
为了方便和快速周转,你会在本地工作站上测试你创建的构建版本。然而,一个功能不应该被认为已经完成,直到它在一个正式的游戏构建版本中被测试过。不要只在Unity编辑器中测试它,也不要只在开发构建版本中测试它。确保它在与整个团队的代码和资源集成后,在一个构建版本中确实能够正常工作。
如果你有一个构建脚本和/或持续集成系统,你的最终测试应该在从该系统出来的构建版本上进行。
测试计划
制定一个书面的测试计划。你将测试什么?预期的行为是什么?你将以什么频率进行测试?你将如何记录测试会话的结果?这些是你应该回答的问题。有一个流程将帮助你保持清醒。
缩短反馈周期
确保从做出更改到能够测试它之间的往返时间尽可能短。这能保证测试快速高效,并且可以频繁进行。让测试变得更容易,会增加你遵守纪律的可能性,测试也更有可能真正被完成。一个快速、方便、自动化的构建过程会有所帮助。但你真正需要的是为测试定制的关卡。为了测试特定功能,你需要一个加载速度非常快的精简关卡,游戏摄像头在正确的位置,并且相关的玩家、道具和NPC都方便地实例化在彼此附近。任何能够最小化从更改到看到更改效果的往返时间的事情,都会让你更高效。
资源与性能预算
你的性能和资源使用目标是什么?你应该对你试图达到的性能特征有所了解。期望的帧率、内存使用量、多边形数量、屏幕上的实体数量等是多少?把这些写下来,否则你会忘记。这类测试相当容易自动化。
保持你的文档更新
我们谈到的所有文档都应被视为动态文档。随着你对游戏、受众的理解加深,以及流程的改进,要不断更新它们。不过,尽量不要让文档变得过于繁重!拥有一个能工作的游戏比拥有一套完整的文档要好,但完全没有文档也不是好事。
测试驱动开发
通过测试来驱动你的开发。这是最重要的技术,也是我们测试理念中不可或缺的一部分,所以我们已经对此进行了相当长的讨论。如果可以,创建单元测试。否则,创建集成测试,它们比单元测试更容易编写和维护,但也非常有效。如果你没有时间或能力编写自动化测试,可以考虑仅使用TDD的原则来指导你的开发,正如我们已经讨论过的。
输出测试
从游戏的测试版本中输出有用的数据,是成本最低的测试方法之一,并且非常适合自动化测试。这通常被称为日志记录。如果你将日志记录到一个人类可读的文本文件中,你可以直观地检查输出来验证游戏逻辑是否按预期工作。
你还可以更进一步。将之前的日志文件提交到你的版本控制系统中,这样你就可以轻松地比较输出来快速了解行为是否发生了变化。
你可以再进一步……让你的游戏处于自动驾驶模式(输入事件),并记录输出。让你的构建系统在检测到日志输出发生变化时给你发邮件。这是我们所知的成本最低的自动化测试方法之一,但仍然非常有效。
自动化
如果你编写自动化测试,请确保它们被安排频繁运行。任何像样的持续集成系统都允许你定期或在事件发生时(例如在版本控制提交时)创建构建并运行测试。
让你的游戏可编写脚本。这并不像你想象的那么难。让它能够加载一系列关卡,并用一个沿着脚本化路径穿过场景的AI摄像机替换正常的玩家摄像机。或者记录玩家输入并回放它们来模拟游戏玩法。
图形测试
如果你有一个完全确定性的渲染器,你可以比较不同构建版本运行的屏幕截图。像 Image Magick compare 这样的程序实际上让这变得很容易。
让你的渲染具有确定性则是另一回事了。如果你想让它起作用,你需要相当大的投入。
确定性
这与其说是一种技术,不如说是你应该努力追求的东西。拥有一个确定性的游戏(或者至少是它需要达到的确定性程度)对于测试以及游戏开发的许多其他方面都非常重要。确定性是指在输入相同时,游戏能够产生可预测结果的能力。换句话说:游戏在完全相同的条件下应该有相同的反应。
如果你没有一定程度的确定性,那你怎么期望重现玩家或QA报告的问题呢?测试驱动开发有助于提高确定性。你还需要一个可靠的随机数生成器(稍后会详细介绍)。
我们已经写了比本文能容纳的更多关于确定性的内容,所以请期待未来关于这个主题的文章。
技术栈
这里总结了我们用来使测试更高效和有效的工具、技术和功能。
构建流程
你的构建流程应该是自动化的,并且100%可靠和可重复。确保团队中的每个人都能轻松地、以最少的设置来创建一个构建版本。你应该能够从命令行运行一个构建,这将使通过持续集成来自动化你的构建变得更容易。
确保开发团队都理解构建过程及其所依赖的工具。开发者对他们的工具如何工作有基本的了解是至关重要的。他们必须具备修改和改进工具的能力,这样他们才能控制和改善自己的工作流程。实现这一点的最佳方式是让每个开发者都在某个时候参与构建脚本的工作。
构建过程应该能够轻松地在CI系统和本地开发者工作站上工作,否则你怎么能测试和调试构建脚本呢?构建过程本身经过极好的测试是如此重要!构建和发布传统上充满了错误。自动化你的构建并频繁练习,你将减少因构建过程导致构建失败的风险。
测试框架及相关库
对于单元测试和自动化集成测试,你需要一个好的测试框架和测试运行器。你可能希望能够从命令行运行单元测试(这样它们就可以包含在你的自动化构建过程中)。你也应该能够在所选的游戏引擎中运行单元测试。为此,你可能需要自己编写一个测试运行器,但这是值得的,因为有些问题可能只有在游戏引擎下运行时才会出现。
在Real Serious Games,我们使用XUnit.net,但它只是市面上众多适用于各种编程语言的框架之一。为了在单元测试中实现真正的隔离,你需要使用依赖注入。然后你可以使用一个模拟(mocking)库来替换你的依赖项。我们使用Moq。
版本控制系统
如果你是专业开发者知道什么对你有好处,那么你已经在使用版本控制了。故事到此结束。版本控制对于软件开发过程是如此重要,以至于在这小小的空间里很难充分说明其重要性。
你应该只提交小的更改。这将使追踪到首次出现错误的修订版本变得更容易。当你能快速追踪到导致错误的更改时,错误调查会变得容易得多。这也会告诉你哪个开发者做了这个更改。与那个开发者交谈,以理解更改的意图和原因。所有这些都有助于你更接近修复错误的方案。
如果你的版本控制系统有二分查找功能(bisect feature),那么就用它来非常快地找到导致问题的修订版本。一旦你开始使用二分查找,你会发现它会影响你的编码习惯,你会开始为了更容易进行二分查找而编码。
在Real Serious Games,我们根据项目、团队和具体情况使用Git、Mercurial或Perforce。
错误跟踪系统
使用一个错误跟踪系统来记录和跟踪你问题的状态。我们目前使用Redmine,一个免费的开源解决方案。如果可能,你应该让事情变得更简单,使用一个单一的系统来同时进行任务和问题跟踪。我们的理由是,修复错误应该与添加新功能具有同等的地位,也就是说,两者都应根据必要性以及它们为游戏带来的价值来确定优先级。
差异比较工具
你需要一种方法来直观地检查文件之间的差异。你需要检查代码修订版本之间的差异,以找到或理解引入错误的修订版本。你可能还想比较输出日志的差异,如前文输出测试中提到的。
我们使用 WinMerge 和 Beyond Compare,但还有许多其他优秀的工具可供选择。
调试工具
你的游戏应该记录所有重要事件。这对于测试非常有用,你可以直观地检查日志来验证游戏逻辑是否按预期工作。更好的是,你应该记录你的日志(例如记录到文本文件或数据库中)。这将允许你事后检查发生了什么,例如,你可以在玩家遇到错误之后进行调查。日志记录对于更高级别的自动化测试非常有用,其形式如下...
- 为你的游戏编写脚本(如前所述)
- 使用代码扫描日志以测试某个事件是否发生。
你的游戏应该输出性能指标。你可以将这些记录到文件中,例如直接记录到CSV文件中,这样你的指标就已经是一种方便的格式,可以加载到Excel中进行分析和绘图!更复杂的方法是:将你的指标记录到一个数据库中,你以后可以查询并用于数据分析(在此我们必须推荐我们的开源数据分析工具包data-forge)。使用数据库意味着你可以构建一个Web应用来显示结果。或者更好的是,利用像Influxdb和Grafana这样的工具来记录和可视化你的性能数据。
花时间思考和改进你游戏中的调试和可视化系统。你应该能够轻松地启用和禁用整个系统。在寻找问题时,使用这样的调试功能将帮助你快速缩小问题范围。在一个虚拟世界中,你应该能够可视化那些通常看不到的东西(比如力矢量)。你会惊讶地发现,仅仅因为你通过调试可视化系统看到了问题,一些错误就如此容易地解决了。
自动化工具
你应该购买或构建自己的事件记录和回放工具。你可以记录输入事件,以便回放玩家的输入来模拟他们的游戏过程。你可以记录网络事件来模拟与网络玩家或服务器的交互。
你的游戏应该在一定程度上是可脚本化的。这可以简单到拥有命令行选项,允许跳转到特定关卡,然后启动事件回放或驱动摄像机沿预录路径移动。更复杂的是:你的游戏可以响应网络命令(通过HTTP或套接字传递),指示它开始一个关卡或启动事件回放。
持续集成系统
自动化工具本身就很好用。它们可以帮助加速你在自己工作站上的流程。然而,当构建和测试在持续集成系统中自动运行时,它们才能真正发挥作用。我们使用Jenkins,它是免费的,并且在软件行业中算是一个标准。你可能还想考虑许多其他选择。
有三种主要方式来利用你的CI系统:
- 每当有人向版本控制系统提交更改时,它就会构建游戏并运行测试。如果某个开发者破坏了构建或测试,团队会自动收到通知。这可以自动化你测试过程的很大一部分,并在游戏被破坏时提供即时反馈。
- 它按计划构建和运行测试。当游戏很大且构建过程耗时较长时,这种方式是合适的。你仍然希望使用第一种选项,在每次有人提交时运行单元测试或其他简单快速的测试,但如果完整的构建过程很长,你可能只想每天进行一次完整的构建和测试。
- 它允许你的同事自助进行构建和测试。团队可以自行服务,随时请求构建和测试。当你有许多项目或复杂的构建流程时,这非常有用……就像我们在Real Serious Games所做的那样……让所有项目在每次提交或每日计划中都进行构建是不切实际的。
你的CI系统会保存你过去构建的记录,包括构建稳定性的历史(例如自动化测试失败的次数)。当你需要了解一个项目的历史时,这些信息可能是无价的。
随机数生成器
在游戏中实现确定性的最简单方法之一是控制你的随机数生成器的种子。重要的是,不同的系统(例如交通、行人等)应有各自独立的随机数生成器。这对于使这些独立系统可预测有很大帮助。这意味着你可以将特定系统与游戏的其余部分隔离开来,并且仍然让它们以相同的方式运行,这种可复现性在调查这些系统中的问题时非常有用。
Unity
让我们简要讨论一下我们在Real Serious Games使用Unity时的测试技术。
构建流程
在Real Serious Games,我们的构建流程经历了多个演进阶段。它最初是一个批处理文件。当它变得更复杂时,我们尝试了Python。在我们开始构建Web应用程序后,我们自然而然地想使用JavaScript和Grunt作为我们的构建系统。随着时间的推移,由于我们的构建需要更复杂的准备和自动化测试,构建脚本变得越来越复杂。Grunt不足以管理这种复杂性(许多相互依赖的构建任务)。我们还尝试了Gulp,它好得多,但也没有满足我们的需求。
因此,我们创建并开源了Task-Mule,这是我们自己的定制自动化系统,能够处理我们现在拥有的复杂且相互依赖的构建任务网络。
我们的CI系统和我们的开发者运行完全相同的构建脚本。它通过Unity的无头模式调用来构建项目。
Unity日志
Unity日志是你的朋友。在调查问题时,它应该是你首先检查的地方。
构建日志也很有用,你会希望将其接入你的构建流程和CI系统。
如果你在构建版本中遇到崩溃,日志可以为你节省一些追踪问题的时间。如果编辑器崩溃,这种情况就更有可能发生。找到编辑器日志文件可能有点棘手。各个平台的路径,包括编辑器和构建日志,可以在这里找到:https://docs.unity3d.org.cn/Manual/LogFiles.html。
转储系统信息
将系统信息记录到文件系统。系统信息可以通过Unity的SystemInfo类来获取。如果你在PC上发布,你可以利用DxDiag命令来了解用户的系统。有时你会遇到只在特定平台、系统或硬件上出现的问题。在这些情况下,了解你的用户正在运行什么系统是非常宝贵的。
单元测试
如今,Unity有一个测试框架,对于Unity 4.0及更高版本,可以在资源商店上找到。在5.3版本中,Unity测试运行器已内置于Unity编辑器中。这看起来非常有用,但当我们开始使用Unity时,这个功能还不存在。我们使用xUnit来编写我们的测试,使用Moq进行模拟。我们使用RSG Factory进行依赖注入。
我们在Visual Studio的XUnit测试运行器下运行我们的测试。我们还有一个手写的测试运行器,用于在Unity下运行我们的测试。为什么这很有用?这意味着我们可以验证我们的代码在正常的.NET框架下以及在Unity的Mono框架下都能工作。有时这两者之间的差异会引起问题。在Visual Studio中运行单元测试既快速又方便,但这并不能保证同样的代码在Mono下也能工作,所以请两者都测试。
通过Moq,我们可以创建模拟对象来模拟单元测试的依赖项。这在处理没有接口或虚函数的Unity类时显然会带来问题。我们使用一个简单的抽象层来使得模拟Unity API成为可能,尽管这增加了一层间接性,并不理想,我们正在研究我们可能在未来文章中介绍的替代方案。
持续集成
我们使用Jenkins来提供持续集成服务。Jenkins会监控代码仓库(通常是Mercurial),并在仓库有变更时执行构建和运行测试。
我们的Unity项目(通常使用Perforce)也通过Jenkins进行构建。这些构建通常由自助服务触发,尽管我们的一些构建也安排在夜间进行。
日志与遥测
日志记录是一个宝贵且出奇简单的测试工具。Unity有一个内置的日志API,可以让你入门,但自己动手做一个更灵活、更强大的系统其实并不难。我们的日志系统基于.NET 3.5版本的Serilog。
我们还编写了各种支持工具来帮助我们监控实时日志和查询存储的日志。 LogServer 是一个微型的node.js服务器,它通过HTTP接收日志并将其存储在MongoDB数据库中。 LogViewer 从数据库中读取日志并在网页中显示。通过这个系统,我们可以非常方便地将来自多个构建版本、不同设备甚至不同平台的日志流汇集到一个可搜索的系统中。
我们的指标系统与我们的日志系统类似。我们可以将事件和数据输出到服务器,在那里它们被存储在数据库中。这对于了解我们的帧率和其他指标随时间的变化非常有价值。我们可以测量设备温度、内存分配、玩家位置和方向,以及任何其他可以打包到JSON有效载荷中的值。
因为日志和指标存储在数据库中,我们能够查询和检查来自先前应用程序实例的数据。在问题被记录后能够查询数据库以获取信息,这种能力非常强大。我们可以追溯时间,分析该玩家会话期间发生了什么。这个系统可以远程操作,这意味着无论我们的用户身在何处,我们都能了解他们和他们的问题。时间和距离不再是过去的障碍。
如果你对在Unity中使用Serilog感兴趣,请查看Ash的fork的.NET 3.5分支。但请注意,让这样的代码在Unity下工作是一项只有训练有素的专业人士才能完成的绝技,孩子们请勿在家尝试。
JSON序列化
你可能需要一个好的JSON序列化器来满足你的一般序列化需求。拥有一个JSON序列化器对于捕获事件(用于以后回放)和前面提到的输出测试也很有用。Unity 5.3(终于)自带了自己的JSON序列化器,我们很想尽快试用一下。到目前为止,我们一直使用JSON.NET。它非常棒,但在Unity下使用可能会很困难。你可以在这里找到一个在Unity下工作的fork,尽管我们听说这在iOS下无法工作。
随机数生成
你可以为Unity的随机数生成器设置种子。这是拥有一个确定性游戏的好开始。但不幸的是,这个系统是全局的,所以一个单一的随机数流必须被所有系统共享。这可能导致系统之间相互干扰,并产生不可预测的行为。这意味着你无法有效地隔离你的系统(例如,你无法单独拿出你的行人系统并让它以同样的方式运行)。
在Real Serious Games,我们使用 SimpleRNG 来生成我们的随机数。
结论
在本文中,我们探讨了应用于游戏开发的软件测试。这实际上只是软件测试,我们根据游戏开发者的关切进行了适当的调整。
你已经了解了我们的测试理念,以及一些可以让你有效利用测试的技术和工具。虽然这里讨论的测试技术可以用于任何语言或游戏引擎,但我们已经展示了其中一些如何应用于Unity。
我们希望你现在能更好地进行测试,并能体会到测试的价值。至少,我们已经让你对此有了更多的思考。在最好的情况下,你现在可以彻底改变你的整个开发流程,并以测试为核心来推动它前进。
如果你从本文中没有学到别的,请记住这些事情:
- 测试不能留到最后。这样做会将风险延迟并累积到项目末期,那时它能造成最大的损害。
- 测试将帮助你交付一个高质量的游戏,它能按预期工作,没有重大问题,并在性能限制内运行。
- 测试驱动开发可以(而且可能应该)指导你的开发,并极大地改善你的流程(我们不只是在谈论单元测试!)。
最后... 只管做些测试。
关于作者
Ashley Davis
Ash是一位经验丰富的软件开发者,自1998年以来一直专业从事游戏开发,期间曾在其他行业有过几次短暂的经历。Ash现在在Real Serious Games构建严肃游戏、模拟和VR体验。Ash也以Code Capers的名义作为承包商,构建了跨多个平台的产品。
Ash为开源社区做出贡献,并且是布里斯班游戏技术和布里斯班游戏开发的创始人和组织者。Ash也帮助组织布里斯班Unity开发者活动。
更详细的个人简介,请参见Ash在LinkedIn上的个人资料。
Adam Single
Adam是一位充满激情的游戏开发者。Adam曾是RSG的员工,现在是Well Placed Cactus开发团队的资深成员。他是布里斯班游戏技术、布里斯班游戏开发和布里斯班Unity开发者聚会的组织者。
他还是一个名为OneCoin Creations的小型两人团队的联合创始人。作为父亲,他们在工作和家庭生活之余开发自己的游戏。
更多信息请参见:https://branded.me/adamsingle
关于审阅者
非常感谢我们的审阅者,是他们帮助这篇文章成为现在的样子。
Mark Hogben
Mark是一名拥有超过十六年经验的专业软件工程师,其中大部分时间都在游戏行业度过。他目前正在开发自己的游戏项目,并撰写他的第二部小说。
Leigh Mannes
Leigh是Well Placed Cactus的技术主管。在加入团队之前,他曾在全球各地的机构和组织中,从事移动、在线和现实世界体验式装置的开发工作超过十年。