《我的世界》首席软件架构师分享:自动化测试的6个经验心得

【GameLook专稿,未经授权不得转载!】

GameLook报道/如今大部分游戏都是网络游戏,或者更确切的说,是服务型游戏(GaaS)。这些游戏的更新频率更高,仅靠人工测试效率越来越低下,越来越耗时,而且成本越来越高。很多工作室都在尝试自动化测试,但往往发现创造快速、可靠并且真正有价值的自动化测试,是一场看不到结束的艰难战斗。

在此前的GDC大会上,微软旗下Mojang工作室首席软件架构师Henry Golding分享了《我的世界》自动化测试的经验和技巧,他还曾是《盗贼之海》自动化测试负责人。

以下是Gamelook听译的完整内容:

Henry Golding:

我是来自Mojang工作室的Henry Golding,我们将要分享的是,当尝试用《盗贼之海》同样的方法测试《我的世界》时,我们学到了什么。我会尝试总结一些方法和技巧,希望能够对其他项目的自动测试也能带来帮助。

我从事游戏行业已经12年了,过往的经验基本上平均分布于玩法、程序和自动测试三个领域。我参与过多个项目的研发,和今天相关的是曾经在《盗贼之海》项目带领自动测试框架团队,目前在《我的世界》团队担任同样的职位。

为何要做自动化测试?

在我从业期间,我们发布游戏的方式发生了很大的变化:刚开始的时候,我们把游戏写到一张磁盘上就结束了,后续还可以发布补丁、DLC。整个游戏行业趋势转向了GaaS,游戏发布更新越来越频繁,《盗贼之海》与《我的世界》都是很好的案例。

行业趋势向服务转变带来了很大的问题,自动测试变得至关重要,游戏已经大到不可能人工测试了。我们过去曾经只依赖于人工测试,当你只想做一两次测试的时候,代价可能是有些昂贵的,但当你经常发布更新的时候,就需要不断地支付这个费用,测试成本就会大到难以承受。

对于《盗贼之海》和《我的世界》这样的即兴玩法沙盒游戏来说,很难在有信心发布的同时做到比较高的测试性价比,所以很多工作室都在寻找另一个方式。

在《盗贼之海》这个项目上,我们解决这个问题的方法就是采用持续交付,在2018年的GDC大会上,Jafar Soltani讲述了这个方法,以及我们通过它得到的帮助。基本上来说,持续交付就是能够在任何你想要的时候发布内容,因为你总能将代码保持在稳定状态。当然,这件事说起来容易做起来难,但你可以想象,自动测试是持续交付的基础,因为它可以让你的代码维持在稳定状态。

自动化测试是《盗贼之海》项目研发很重要的一部分,我们知道,为了实现持续交付,我们需要以快速可靠的方式验证游戏,所以我们打造每个功能都要证明它们是行之有效的,2019年的时候,Robert Masella专门就这个问题做了一次演讲。可能有人记得这次演讲,因为在GDC历史上,首次有人把玩法用函数形式写出来。

自动化测试很重要的一个功能,就是谁来写测试,那就是让游戏开发者来写测试代码,包括玩法代码,我觉得这是最有效率的方法。在软件行业,这基本上也是所有领域的标准做法,而且开发者也可以对给玩家带来高品质体验投入热情,当有了对的工具和训练,我们实际上可以将它做的非常好。

《盗贼之海》与《我的世界》是有些共同点的,但也有些关键的差异。对于《盗贼之海》,我们可以完全从头开始,游戏是从一个小团队开始研发的,所以我们可以从一开始写代码的时候就保证它能用来测试。《我的世界》是一款十多年的老游戏,它的代码并不是为了测试而设计的,然而我们始终都有数百名开发者对其做出改变。

然而它们也有些相似性,由于开放世界属性,两款游戏都很难讲明白,而且两者都频繁发布更新内容,所以都很适合持续内容交付。刚开始的时候,两个团队都不熟悉自动化测试,所以我们可以开始就在两个团队都形成一个文化。

目标:让开发者写测试

假设我们都对持续内容交付很感兴趣,并且希望我们的开发者们写出快速、可靠的自动化测试成为他们日常工作的一部分,在他们写代码的时候,就能快速找到bug所在。那么,我们该怎么做到呢?

这是很有挑战性的,我见过这样的尝试失败,也见过成功的,不过,我们在《盗贼之海》和《我的世界》都做成了,接下来我就说一些在两个项目上都有用的主题和方法。

《我的世界》自动化测试技巧

简短来说,它们分别是:让一个小团队负责过渡、满足代码库的需求、偏重采用,然后规模化、让开发者作为合作伙伴一样参与其中,以及从领导层得到对应的支持。

1、安排团队负责过渡

采取自动化测试通常试一次新奇未知的冒险,当我们不适应这种方式的时候,需要克服许多的障碍,需要有人来带来这种文化上的改变。需要有专门的团队写专门的工具或框架,教开发者们使用,以及持续提供的支持并打造社区,任何一种情况下,两三人的团队就已经够用了。

为了给下一个方法打下基础,我们来探索写测试的一些不同方式。我注意到,作为开发者,当我们开始思考自动化测试的时候(包括我也不例外),都倾向于以玩家角度进行测试。这是非常自然的现象,因为传统意义上,我们就是通过自己亲身体验来测试游戏的,因此在设计自动化测试的时候,我们很容易参考人工测试的方法。

对于这些测试,我们给出了很多种名字,功能测试、集成测试、端到端测试,这是很常见的测试起点,《我的世界》最初也是这么测试的。尽管这样的测试有成熟的框架,但通常情况下,这些测试往往很慢,而且不可靠、很难规模化。

由于测试需要加载游戏,所以我们需要等待一些时间才能开始游戏测试,通常是需要数十秒或者更久。之所以说测试结果不可靠,是因为同时加载了太多与我们测试目的不相关的代码,但有很多bug会导致我们的测试失败,因此,你很难确切地知道某次测试为什么失败,实际上,我们的测试方法是有问题的。

这些测试写起来也会很难,由于我们需要用间接的方式搞定测试。比如,如果我们想要在游戏世界里测试(武器的)格挡功能,就需要把它放到玩家背包、让玩家装备它,盯着特定方向,然后使用格挡。假如我们只是想要测试格挡,就不必要地加载了背包和玩家相关的很多代码,这就导致在测试格挡功能的时候有很多因素会导致失败。

作为程序员,我们很幸运地可以对单个物品进行测试,我们必须要像人工测试那样通关整个游戏,而是可以调用一个函数进行单元测试。单元测试主要是对特定一批代码进行测试,这批代码是与游戏的其他部分隔离开的,这是测试策略很基础的部分。

单元测试速度很快,因为不需要处理大量不相关的代码,所以每次测试可能只需要几毫秒;它们也非常可靠,因为它只会执行我们真正在测试的代码。这种方法也很容易写,不仅是因为设置起来更容易,还因为我们可以直接调用需要测试的代码,所以我们非常熟悉API,因为我们已经写好了。

然而,当写代码的时候就考虑测试,那么做单元测试就会有些困难,因为这通常需要重构引擎级的代码。即使我们写了引擎代码、也愿意进行修改,如果没有开始的测试,就可能会带来bug,所以我们经常会发现重构代码来进行单元测试太过于冒险。所有这些都让单元测试几乎是不可能的,至少是大部分游戏代码都不太可行。

所以,《我的世界》当中测试也是一样,即使我们加入了功能测试框架和单元测试框架,我们想要将它写到任何测试当中使用也是有困难的。这里举一个单元测试有关的架构问题:

假设我想要测试一个组件的方法,要创造组件就需要有Actor,这就需要有一个关卡,而想要做一个关卡,就需要主游戏类,而这太大而且太复杂,只有通过运行游戏才能得到,因此我无法对组件进行单元测试。

2、满足代码库要求

当功能或者集成测试不理想的时候,就不可能进行单元测试,所以,我们现在就需要一些满足代码库要求的框架,并且为游戏代码解锁单元测试。

我们在《盗贼之海》和《我的世界》使用都非常成功的一个技巧,就是在单元测试和功能测试之间的“中间”测试,随后我会介绍它在两个项目中看起来是怎样的。

《盗贼之海》使用的方法我们称之为Actor测试,它将引擎的可解锁或可修改部分作为首要类依赖关系。因此,如果你需要一个关卡复活一个Actor,就需要一个真正的关卡才能做到,而不是尝试模仿这个关卡。

所以,不要破坏依赖关系让它独立,相反,你需要满足依赖关系,然后写那些看起来像是单元测试的代码,这些写起来简单、可靠而且容易。

这里Robert局的例子当中,他测试的是白天的影之骷髅,正确地将其从黑暗切换到了光状态,我们可以看到,他只是用了几行同步代码就实现了这一点。他首先创造了一个影之骷髅,并且确保它是在黑暗状态,然后将游戏世界改成中午,然后通过调用光将影之骷髅设定为光状态。

通过这几个简单的调用代码,他有效地对自己的代码进行了单元测试,即使他的代码并不是与引擎分离的,所以这本质上并不是真正的单元测试。这些类型的测试很容易写,运行很快而且可靠,也是单元测试和功能测试很好的平衡,因此它们占据《盗贼之海》70%的测试也就不令人意外了。

对于《我的世界》,我们通过《盗贼之海》的Actor测试借鉴灵感,创造了一种我们成为服务器测试的方法。在服务器测试中,我们通过加载平坦的游戏世界来满足引擎依赖关系,并且让测试可以触及到它。

之所以称之为服务器测试,是因为我们现在很多的测试都是运行在客户端,但有趣的事情却发生在服务器侧,但令人意外的是,它与Actor测试很像。

这个案例中,我们想让蜂巢在被标记之后不再生出蜜蜂,如Robert的案例那样,只需要几行代码就能完成。首先我们生成一个蜂巢Actor,然后直接加上标记方法,然后,我们通过直接调用Actor,让它不想再生出蜜蜂。

这种方法不受太多因素影响,而且直接调用方法,因此它不需要等待时机,也不需要主要的因素和引擎分开,因为我们提供了一个真正的关卡并复活了真实的Actor,才运行了单元测试。如果是用功能测试,就需要做更多的工作,因为我们需要找到蜜蜂、计算世界里的蜜蜂数量,然后再次计算,确保我们的蜜蜂数量不变,这不仅难写,也会需要很长时间运行,还可能有更多地方出错。

所以,就像Actor测试那样,服务器测试非常方便,我们目前在《我的世界》用的超过4000个,占据了这款游戏测试总量的35%。

如果今天一定要强调某件事,我觉得单元测试和功能测试之间的“中间测试”就是最重要的,它可以对旧代码进行测试,而且也应该可以用于任何一款游戏。目前为止,我们发现这个方法在虚幻引擎上可以使用,也能在《我的世界》引擎上运行。

3、偏重采用

对于采用,我指的是持续、广泛的测试授权,所以开发者在为他们大多数的改变写代码。为少数人做少量测试写框架相当容易,但是,想要达到让每个人大部分时间都使用它,是非常有挑战性的。而且,我觉得这是任何测试框架当中,最重要也最优先的问题。

比如,在之前一个测试当中,我写了一个自己觉得很好而且很完美的框架,甚至还简单易懂。悲剧的是,没有人使用这个框架,因为在这里面写测试有太多的分歧,你需要打开一个单独的项目用两种语言写代码,然后还需要写游戏代码将功能暴露给框架,测试结果也不那么可靠。所以,我在《盗贼之海》汲取了教训,写了一个真正让人们使用的框架。

在《我的世界》项目上,我们的框架已经内置在游戏里,因此不存在那样的分歧,但依然因为其他原因存在一些分歧。因此,我们第一步就是承认这不适合我们,即便游戏里已经有一个投入努力就可以用的框架,可是并没有人真正用它来写测试。

所以,我们聚焦于写一个真正实用的框架,甚至要考虑技术债,满足开发者们的要求,让它写一个测试比不写测试更容易用,因为我们认为,帮助人们走出第一步是写测试很重要的一方面。做到这些远不只是易于测试的框架,还需要做出文化上的改变,有对的框架是很必要的。

4、规模化

采用之后就是规模化,我们发现遇到了新的、更好的问题,比如有很多测试耗费的时间比我们想象的更长。为了节约测试时间,《盗贼之海》支持世界旅行,以减少测试初始化占用的时间,对于《我的世界》我们将同一个关卡的测试分组,以尽可能避免重复加载一个关卡。

这给我们减少了90%的时间,但也是有代价的,比如不同测试之间的状态泄露,如果解决不当,就有可能导致测试失败,因为这个关卡之前已经运行过了,而且这种失败很难追踪。

大量的测试会导致很难搞清楚其中一个失败的原因,假设当你复活一个Actor的时候有1%的概率导致游戏崩溃,任何需要复活这个Actor的测试都可能受到影响,考虑到我们的测试数量之多,遇到这个崩溃bug的概率就会高很多。为此,我们在《我的世界》测试中增加了自动重试功能,所以在被认定为真正失败之前,这个测试必须自动重试,这帮助开发者减少了噪音。

两款游戏都使用了隔离方法,主要是为了确保测试结果可靠。当然,这些问题只影响服务器或者功能测试,所以我们鼓励优先采取单元测试,以避免这些“成长的烦恼”。

5、让开发者们参与

接下来我想聊聊开发者们在这种转型当中的角色,虽然两个案例中我们都只投入了很小的团队进行过渡,但真正成功的是我们与开发者们互动的方式:我们把他们当做客户与合作伙伴。

就像测试游戏那样,框架和类似过程都需要开发者们的反馈,更重要的是对这些反馈做出行动,建立信任并不断迭代。不同的开发者可能有不同程度的参与,但在《盗贼之海》和《我的世界》自动化测试当中,很多开发者做出了大量贡献。有些人只是给出反馈,还有人补充了他们团队需要的框架。

在人们对新工作方式不确定之后,担心这可能需要投入大量人手、甚至可能是浪费时间,我发现人们从最初的尝试者变成最后的积极推广者,最重要的转折点就是他们在自己的代码中找到了bug,这似乎是一个皆大欢喜的体验,转变了人们对自动化测试的态度。

我还认为,虽然测试框架现在看起来对我们是新鲜事物,但却是非常必要的工具,就像IDE或者编译器那样。我发现,不同的团队开始打造自己的测试框架,哪怕专门有团队是做测试框架的,我觉得这些团队的最终目标,就是帮助他们的客户最终实现不需要他们继续参与其中。

因此,我们团队很重要的一个活动,就是帮助这种升级的实现,用他们的知识和研发计划帮助社区打造。两个案例中都很好用的一个小技巧,就是测试审核者,他们知道如何进行测试,并且可以为其他人写代码。

6、领导层支持

领导层的支持可以成就或者破坏一个文化变革。在《盗贼之海》和《我的世界》项目中,我们很幸运有这种支持,我希望强调这种支持带来的一些帮助。

首先,我们的团队领导相信并成立了自动化测试团队,这是不常见的研发投入。他们还相信开发者投入时间写自动化测试的价值,这更多是一种信念,因为我们很难在当时证明这些价值。

即使有领导层支持,仍然会有意外发生,还需要注意预期管理。这有可能会延缓甚至迟滞研发进度,很容易带来自动化测试是否值得投入的问题,所以我觉得领导层理解并改变研发团队工作方式是很重要的,它会在短期内让研发变慢,但长期优势非常大。

我们还看到有些领导希望看到整体游戏品质提升的即时结果,基于我们的经验,我认为评估它是否有效比较合理的时间是一年。取决于你想要开始的代码数量,这可能会需要更长时间。如果你有大量的未测试代码,最好的方法就是先测试新代码和有bug的部分,而不是尝试给所有遗留代码做测试。

我看到另一个现象,就是将测试与研发分离,这通常意味着测试工作给更多新功能研发让路。我们将测试作为每个功能可分离的一部分,并且将其作为评估功能是否完成的一部分。事实胜于雄辩,如果领导层重视测试,却又想要看到短期效果,那么开发者就要将功能研发的优先级置于测试之前。当然,开发者们也需要做好与领导的沟通,告诉他们这种选择需要做出的变通,让领导听到反馈,然后给开发者们尝试的空间。

这是《我的世界》在2019年10月加入自动化测试之后的结果,三条线分别是单元测试、功能测试和服务器测试。可以发现,直到2020年4月左右服务器测试次数才开始增长,因为这时候刚好是我们与开发者互动,让写服务器测试变得更加简单。

随后,可以看到这些测试稳定增长,因为大部分开发者如今可以写他们大部分的测试工作,最近的开发者调查显示,我们的自动化测试框架比《我的世界》任何研发部分的好评率都更高,我觉得这说明了我们的努力是有成效的。

比较有趣的是,服务器测试似乎推动了单元测试,这是我没有想到的。我们认为,服务器非常便捷而且容易获得测试机巧,随后他们会喜欢真正的单元测试带来的帮助,继而会更有动力让系统适合单元测试。

然而,我们还有很长的路需要走,我们仍然有团队得不到框架的加持,有些规模化的难题还没有被解决,在《我的世界》项目上,我们的自动化测试才刚刚开始,但我们都对选择的方向感到乐观。

将我们的研发方法现代化是一个真正的挑战,但我相信自动化测试是控制游戏研发测试成本、避免未来过渡加班唯一的方式。

如若转载,请注明出处:http://www.gamelook.com.cn/2022/09/497805

关注微信