大家好,我 Jim ‘Anodoin’ Merrill,我的工作是致力于英雄联盟的自动化测试,特别关注的是游戏中的体验。我现在担任一个技术团队的负责人,致力于构建验证系统开发(BSV-Dev)团队。主要工作是构建自动化测试工具,帮助其它团队书写更棒的测试。

在过去的几年当中,我们致力于改良我们的测试系统及基础设施,来提高开发人员效率以及减少上线的 bug。现在我们每天要跑 10W 个测试用例,这个数量的测试用例可以让内容以更少的 Bug、更快的的呈现给玩家。我想来分享一点我们所做的工作,希望能开启一段游戏领域的自动化测试的交流。

为什么我们关心?

英雄联盟更新非常非常快。我们平均每天能看到 100 处以上代码或内容变更提交到版本控制系统,想要对所有这些修改提供充分的覆盖是一个挑战。由于每两周更新一个补丁,快速发现漏洞至关重要。如果在发布过程中 bug 发现的晚了可能会导致发布延迟,甚至重新部署或者需要暂时禁止某个英雄等,这些都是非常不好的玩家体验。自动化解放了我们的质量分析师(QA)使他们可以关注更有创造性的测试以及上游缺陷的预防,这里他们可以提供更多的价值。

自动化也能更快的反馈测试结果。每次代码或内容提交都人工扫一遍所有测试是非常不可行的,如果非要这么做,那需要一支测试军队来保证足够快得返回结果。

我们的测试系统在持续集成(CI)上运行而且提交后 1 小时内返回报告。这意味着开发人员可以在一个合理的时间内得到测试结果,这有助于减少上下文的切换;事实上,自动化发现的 bug 解决的速度是平均 bug 的 8 倍。更好的是,如果我们需要增加测试的吞吐量,我们可以简单的向测试场中添加执行器即可。

构建验证系统

这个富有想象力的命名——构建验证系统 (BVS) 是我们针对游戏的客户端及服务端的测试框。他负责载入测试用组件,并部署到测试机,启动并管理测试,执行测试,并报告其结果。这些测试与组件都是 Python 编写的,我们编写了大量的 BVS 代码来使测试的编写者们能从复杂的收集收集依赖的过程中解脱出来。最后,只需要测试类中的几个参数,就可以指定运行那个地图,加载多少个客户端,以及游戏在什么级别的联赛中。

测试使用远程过程调用(RPC)端点暴露在客户端及服务端,以便执行命名以及管理游戏状态。大部分情况,测试包括了一个线性的指令和查询的集合——现有的测试覆盖了从英雄技能到视觉规则到击杀小兵期望获得的奖励。我们更早的一些测试是非线性的,这对一些技术稍差的开发人员难度要大得多。

由于一个测试环境的所有的配置工作都是隔离的,不论是本地测试环境还是测试场效果都是一样的。这样当对游戏做出修改的时候就可以很方便的在本地跑测试。

例如,我们对 Kog’Maw 的新的W技能写了如下的测试:

  1. """
  2. Name: BioArcaneBarrage_DamageDealt
  3. Description: Verifies the damage modifications from Bio-Arcane Barrage
  4. Verifies:
  5. - KogMaw deals less damage to non-lane minions
  6. - KogMaw deals percentile magic damage
  7. - KogMaw deals normal damage to lane minions
  8. """
  9. from KogMawAbilityTest import KogMawAbilityTest
  10. from Drivers.LOLGame.LOLGameUtils import Enumerations
  11. import KogMawStats
  12. class BioArcaneBarrage_DamageDealt (KogMawAbilityTest):
  13. def __init__(self, championAbilities):
  14. super(BioArcaneBarrage_DamageDealt, self).__init__(championAbilities)
  15. self.ability = 'Bio-Arcane Barrage'
  16. self.slot = KogMawStats.W_SLOT
  17. self.details = 'Kog\'Maw deals reduced base-damage to non-minions with additional percentile damage'
  18. self.playerLocation = Enumerations.SRULocations.MID_LANE
  19. self.enemyAnnieLocation = Enumerations.SRULocations.MID_LANE.angularOffsetDegrees (45, 200)
  20. self.enemyMinionLocation = Enumerations.SRULocations.MID_LANE.angularOffsetDegrees (45, 400)
  21. def setup (self):
  22. super(BioArcaneBarrage_DamageDealt, self) .setup ()
  23. self.enemyAnnie = self.spawnEnemyAnnie (self.enemyAnnieLocation)
  24. self.enemyMinion = self.spawnEnemyMinion (self.enemyMinionLocation)
  25. self.teleport (self.player, self.playerLocation)
  26. self.issueStopCommand (self.player)
  27. def execute (self):
  28. self.takeSnapshot ('preCast')
  29. self.castSpellOnTarget (self.player, self.slot, self.player)
  30. self.champAttackOnce (self.player, self.enemyAnnie)
  31. self.takeRecentDeathRecapSnap (self.enemyAnnie, "annieRecap")
  32. self.resetCooldowns (self.player)
  33. self.castSpellOnTarget (self.player, self.slot, self.player)
  34. self.champAttackOnce (self.player, self.enemyMinion)
  35. self.takeSnapshot ('minionRecap')
  36. self.teleport (self.player, Enumerations.SRULocations.ORDER_FOUNTAIN)
  37. def verify (self):
  38. # Verify that enemy Annie is taking the correct amount of damage.
  39. annieAutoDamageEvents = self.getDeathRecapEvents (self.player, "Attack", "annieRecap")
  40. annieAutoDamage = 0
  41. for event in annieAutoDamageEvents:
  42. annieAutoDamage += event.PhysicalDamage
  43. annieSpellDamageEvents = self.getDeathRecapEvents (self.player, "Spell", "annieRecap", scriptName=KogMawStats.W_MAGIC_DAMAGE_SCRIPT_NAME)
  44. annieSpellDamage = 0
  45. for event in annieSpellDamageEvents:
  46. annieSpellDamage = event.MagicDamage
  47. AD = self.getStat (self.player, "AttackDamageItem")
  48. expectedPercentile = (KogMawStats.W_AD_DAMAGE_RATIO * AD)/100
  49. annieTotalHealth = self.getStat (self.enemyAnnie, "MaxHealth")
  50. expectedPercentileDamage = self.asPostResistDamage (self.enemyAnnie, expectedPercentile * annieTotalHealth, 'MagicResist', snapshot='preCast')
  51. self.assertInRange (annieSpellDamage, expectedPercentileDamage, expectedPercentileDamage * .1, "{} magic damage dealt. Expected ~{}".format (annieSpellDamage, expectedPercentileDamage))
  52. expectedPhysicalDamage = self.asPostResistDamage (self.enemyAnnie, KogMawStats.W_NON_MINION_DAMAGE_RATIO * AD, 'Armor', snapshot='preCast')
  53. self.assertInRange (annieAutoDamage, expectedPhysicalDamage, expectedPhysicalDamage * .1, "{} physical damage dealt. Expected ~{}".format (annieAutoDamage, expectedPhysicalDamage))
  54. # Verify that enemy minion is taking the correct amount of damage.
  55. AD = self.getStat (self.player, "AttackDamageItem")
  56. minionExpectedPhysicalDamage = self.asPostResistDamage (self.enemyMinion, AD, 'Armor', snapshot='preCast')
  57. expectedPercentile = (KogMawStats.W_AD_DAMAGE_RATIO * AD)/100
  58. minionTotalHealth = self.getStat (self.enemyMinion, "MaxHealth")
  59. minionExpectedMagicDamage = self.asPostResistDamage (self.enemyMinion, expectedPercentile * minionTotalHealth, 'MagicResist', snapshot='preCast')
  60. expectedDamage = minionExpectedMagicDamage + minionExpectedPhysicalDamage
  61. actualDamage = self.getDamageTaken (self.enemyMinion, 'preCast', 'minionRecap')
  62. self.assertInRange (actualDamage, expectedDamage, 1, "{} total physical and magic damage dealt. Expected ~{}".format (annieAutoDamage, expectedDamage))
  63. def teardown (self):
  64. self.destroy (self.enemyAnnie)
  65. self.destroy (self.enemyMinion)

复制代码

Kog’Maw 整套测试的第一部分,包含了 Arcane Barrage 的伤害测试,具体过程如下:

当一个测试运行完,它将测试的结果提供给一个独立的报告服务,那里存储了过去六个月的运行数据。基于测试数据,这个服务采取不同的行动。一个本地运行的测试会在测试执行的机器上打开一个 web 页面,包括通过的与失败的测试用例的详情。如果在测试场运行,手游买号平台当有任何测试用例失败的时候,系统会自动根据测试结果创建 bug 标签及 issue 等,并给提交者发送邮件。测试数据也会通过报告服务聚合及跟踪,使我们可以知道什么情况下测试不通过,以及失败的频率,还有 bug 存在多久了。

在 Wood 5 级的比赛中我们不使用守卫,因此在我看来这个严重故障也没什么问题

出于防止古怪及不可靠测试的目的,每个测试都必须经过一个标准的流程来保证可信。当一个测试经过了代码审查并提交,它加入一个测试集合叫做 BVSStaging。在那里测试在提交运行前必须稳定运行至少一周。如果在 Staging 中的测试失败了,只会通知测试的开发者,来避免困惑。

当一个测试被证明可靠后,它被提交进两个集合中的一个。第一个集合,BVSBlocker,包括的测试指出该构建是否值得进一步测试。如果一个构建在 Blocker 集合测试不通过是不会部署到测试环境的,因为游戏不能开始,或者有好几个会导致服务端崩溃的 bug 影响游戏。相对的,BVS Core,使我们功能测试的核心集合,包括对每个英雄技能的测试。

框架深度游览

BVS 分三个层面实现:执行器,驱动及脚本。执行器为功能测试实现了一套通用的 API,驱动实现的一个测试的配置与执行的具体步骤。最后,脚本实现测试用例的具体逻辑。当下,我们只有一个驱动在使用(LOL 游戏),但是执行器与驱动分离的设计意味着在将来的项目里可以通过实现各自的驱动来使用 BVS 系统,而且可以共享使用 LOL 的驱动编写时编写的工具。

由于一些原因,我不能对流程图要求更多...

个别的组件注册了他们必选和可选的参数,作为它们声明的一部分。当命令行提供了参数,参数被作为字典存储下来,然后组件会在初始化时处理这些参数。BVS 早期的版本使用 Python 标准的 argparse 库,但是出于两个原因,我们选择放弃 argparse 库:第一,潜在参数的数量变得非常巨大,通过系统跟踪变得非常困难;第二,驱动需要有一些驱动特有的参数,这意味着在启动时声明一个解析器是不可行的。

  1. class TestFactory (API.TestFactoryAPI):
  2. requiredArgs = [ArgsObject ('driver', 'Driver you wish to use'),
  3. ArgsObject ('name', 'Name of the test to run')]
  4. optionalArgs = [ArgsObject ('overrideConfig', 'Use a non-standard game.cfg', None),
  5. ArgsObject ('gameMetadataConfiguration', 'A string identifying which game metadata to use', None),
  6. ArgsObject ('listener', 'Log listener to use', None),
  7. ArgsObject ('mutator', 'A string name for mutator to apply to test object', None),
  8. ArgsObject ('testInfoID', 'Test and metadata this test run is related to', None),
  9. ArgsObject ('testSubsetNumber', 'The number out of total if test is subsectable', None),
  10. ArgsObject ('totalSubsetNumber', 'The total numbers of subsets test is split into', None)]

复制代码

一个驱动对象的样例参数

相关的尺度共分为三个等级:测试集合,测试,测试用例。

1、测试集合 是同时运行的一组测试。例如,之前提到的 BVSBlocker 测试集合就是运行在 CI 上的一组冒烟测试。测试集合现在通过 JSON 文件的方式描述给 BVS,可以在 VCS 或 On-The-Fly 中创建。

2、测试 是单独的类实现了一组相似测试用例,使用相同的基础游戏的配置。例如,LoadChampsAndSkins 测试包含了加载各个英雄的资源、皮肤以及确认加载正确的测试用例。

3、测试用例  are 是一个测试中对期望功能测试一个单一单位。例如 LoadChampsAndSkins 中的 loadChampionAndSkin 方法就是一个单独的测试用例,为了覆盖所有英雄和皮肤的组合要执行数百遍。上文提到的 Kog’Maw 整个测试用例被一个更高级别的测试执行,这个测试允许更复杂的测试用例使用比一个函数更复杂的结构。

BVS 并行化通常是在测试集合这一层实现的,但是也可以在测试这一层实现。由于 BVS 通过 JSON 存储和读取测试集合,因此我们可以在 JSON 中创建一个子列表,这样既可以被单个执行器顺序执行,也可以在测试场中并行执行。早期的 BVS 系统中,这个操作是允许我们手工进行平衡的,当测试列表比较小时候,这个比自动化的并行更有效。由于主要测试集合的增长,我们切换到了一个自动化的负载平衡工具来产出这个 JSON 文件,依据每个测试的之前 10 次的平均运行时间来进行调整。

BVS 的大多数用户只跟测试本身打交道,因为我们通过自己的方式来保证测试人员不需要考虑驱动处理的细节。同时,我们用一个非常大的标准类库来封装 RPC 的端点,用于与游戏交互。部分原因是为了防止测试与 RPC 接口过紧的耦合,但主要原因是为了提供一组标准的行为,来防止草率的编写测试,进而保证测试之间的一致性。

特别的 BVS 的标准测试类库不支持纯粹的 sleep。早期的测试编写者,大量使用 sleep,导致一大批脆弱的测试在他们各自执行的硬件上表现完全不同。所有标准类库中的等待操作都是条件等待,都是在等待游戏中一个特定的条件。

  1. @annotate ("Wait until a unit drops the specified buff.",
  2. arguments=[argument ("unitNameOrID", "Unit name (or unique integer unit ID).", (str, int)),
  3. argument ("buff", "Buff you want to drop.", str),
  4. argument ("timeout", "How long to wait.", float, default=STANDARD_TIMEOUT),
  5. argument ("interval", "How often to check for a change.", float, default=SERVER_TICK),
  6. argument ('speedUp', 'Whether to speed the game up.', bool, default=False)],tags=["wait", "buff", "change"])
  7. def waitForBuffLost (self, unitNameOrID, buff, timeout=STANDARD_TIMEOUT, interval=SERVER_TICK, speedUp=False):
  8. conditionFunction = lambda: not self.hasBuff (unitNameOrID, buff)
  9. return self.__waitForCondition (conditionFunction, timeout=timeout, interval=interval, speedUp=speedUp)

复制代码

另一个我们对 BVS 做的重要的适应是由于早期分离出了除了了运行测试之外的所有逻辑。过去 BVS 决定使用什么设备,标记构建通过还是失败,以及排版测试报告。为了保持一个职责的清晰划分,我们分离出了一个服务来处理与运行测试并不直接相关的所有内容。这个服务是一个 Django 应用,使用 Django REST 框架来提供了一组 API 供 BVS 及其它服务使用。

运行及预运行流程

总体性能

总的来说,BVS 对每个英雄联盟的新构建会在大约 18 分钟的时间里运行大约 5500 个测试用例。一天总共运行大约 10 万个测试用例。从一个缺陷提交到 BVS 的第一份失败报告之间的平均时间大概是一到两个小时。50% 的 critical 及 blocker 级别的 bug 都是 BVS 系统发现的,其余的通过内部 QA 或者 PBE 发现。没有被 BVS 发现的问题通常是由于测试覆盖不足的问题,而不是因为差的测试编写。

虽然我们发现的大多数 Bug 都输入功能缺失或游戏崩溃的领域,偶尔我们也会成为一些真正优秀的 bug 的第一发现者。我个人的得意之作发现一个缺陷:游戏中所有的塔都缓慢移向地图的右上角,结果紫色基地的圣塔附近就出现了交通拥堵。我们的发现也包括一些非自动化测试可能无法发现的东西,比如曾经有个问题是点射技能可能会穿过一个敌人,如果英雄恰好攻击了敌人空白点的范围。

总体来说,自动化测试代替手工测试并不是必要的,但是它帮助我们加快了开发的反馈循环而且解放了更多的手动测试人员使他们可以关注更有害的问题的测试。随着英雄联盟内容的增加,我们持续增加更多的覆盖,这样可以提高我们的对缺陷的命中率,也让我们对构建的健康程度更有信心。

谢谢你花时间来阅读这篇文章,如果有问题可以在评论中直接提问。我们下一篇关于自动化测试的文章中将会讨论测试的吞吐量及返回的速度。

揭秘《英雄联盟》的游戏自动化测试相关推荐

  1. 揭秘英雄联盟的数据服务器

    揭秘英雄联盟的数据服务器 Hey,大家好!我是 Bill "LtRandolph" Clark,一名英雄联盟的游戏工程师.许多 Rioter 工程师关注大量的内容需要直接发送给玩家 ...

  2. web网页设计期末课程大作业~超高仿英雄联盟LOL游戏官网设计与实现(HTML+CSS+JavaScript)...

    仿英雄联盟LOL游戏官网设计与实现(HTML+CSS+JavaScript) 关于HTML期末网页制作,大作业A+水平 ~游戏网页作业HTML+CSS+JavaScript实现,共有游戏首页 等页面! ...

  3. python游戏代码怎样才能玩好英雄联盟_用Python编写代码分析《英雄联盟》游戏胜利的最重要因素...

    原标题:用Python编写代码分析<英雄联盟>游戏胜利的最重要因素 点击上图查看 Python Web 开发入门实战[教学大纲+教学进度表] 介绍 在过去的几年里,电子竞技社区发展迅速,曾 ...

  4. python+nodejs+php+springboot+vue英雄联盟的游戏代练系统

    英雄联盟,是当今比较流行的网络游戏,很多人都在线进行英雄联盟的游戏,提高玩家的娱乐性.随着游戏的不断更新,玩家的数量不断增加,游戏代练的行业也逐渐壮大,通过网站进行游戏的代练,能够更加方便人们对游戏的 ...

  5. 机器人钩阿木木_英雄联盟:游戏里机器人最不敢,勾这四位英雄,最后一位不敢勾!...

    原标题:英雄联盟:游戏里机器人最不敢,勾这四位英雄,最后一位不敢勾! 英雄联盟:游戏里机器人最不敢,勾这四位英雄,最后一位不敢勾!英雄联盟已经经历十多个年头,但是他的热度依然不减,深受广大青年的喜爱, ...

  6. [译]揭秘英雄联盟的自动化测试

    原文:AUTOMATED TESTING FOR LEAGUE OF LEGENDS 作者:Jim Merrill 译者:杰微刊兼职译者缪晨 大家好,我Jim 'Anodoin' Merrill,我的 ...

  7. 用 Python 详解《英雄联盟》游戏取胜的重要因素!

    作者 | DeepHub IMBA 责编 | 王晓曼 介绍 在过去的几年里,电子竞技社区发展迅速,曾经只是休闲娱乐的电子竞技产业,到2022年有望创造18亿美元的收入. 虽然在这个生态系统中有很多电子 ...

  8. 用Python编写代码分析《英雄联盟》游戏胜利的最重要因素

    来源:DeepHub IMBA 本文约2600字,建议阅读5分钟. 英雄联盟最重要的获胜条件是什么? 介绍 在过去的几年里,电子竞技社区发展迅速,曾经只是休闲娱乐的电子竞技产业,到2022年有望创造1 ...

  9. 连AI都在看《英雄联盟》游戏直播

    本文来自AI新媒体量子位(QbitAI) 打游戏和看人打游戏,都是一种乐趣. 最近,吃鸡主播约战的事情峰回路转,最终还是没能上演.不光有人在游戏里使用外挂,看游戏直播的也有"外挂" ...

  10. 用Python编写代码来理解赢得《英雄联盟》游戏的胜利的最重要因素

    介绍 在过去的几年里,电子竞技社区发展迅速,曾经只是休闲娱乐的电子竞技产业,到2022年有望创造18亿美元的收入.虽然在这个生态系统中有很多电子游戏,但很少有游戏像<英雄联盟>那样成为社区 ...

最新文章

  1. AWS — AWS 上的 5G 网络及 MEC 边缘计算商业案例
  2. 不允许从数据类型 ntext 到数据类型 varchar 的隐性转换
  3. Linux之CentOS找不到configure
  4. 串操作指令及其应用程序的设计与调试运行——汇编实验四 用8086汇编完成下题 编制一程序,从键盘输入两个长度不同的字符串,设字符串长度小于25个字符。要求在屏幕上以右边对齐的形式显示出
  5. vue+filter过滤器(多参数)传参 - 代码篇
  6. 测试面试题集-1.测试基础理论
  7. 手工给alv添加合计
  8. Java实现分类文件拷贝2
  9. Rust : 闭包、move、复制与移动语义
  10. 【软件】如何批量手机号码归属地查询并且快速分类?批量号码归属地告诉查询分类系统怎么使用?全部教会你
  11. 24个最新创意进度条设计,分分钟让你灵感爆表!!!
  12. Pomodoro Do - 拖延症患者的福音
  13. java横线_java 字体 加横线
  14. 嵌入式web服务器lighttpd的交叉编译及配置
  15. 线性表中的尾插法双链表的学习
  16. 模式识别技术漫谈(5)
  17. python模拟银行存取款_python 模拟银行转账功能过程详解
  18. HTMLcss怎样让文字覆盖在图片上面,图片上加字
  19. 软件的4种维护(更正性、完善性、预防性、适应性)的区别
  20. 10进制转换成2进制,新手制作,欢迎点评。

热门文章

  1. Xiaojie雷达之路---TI实战笔记---OSAL layer说明
  2. C语言 汇总笔记(小甲鱼:带你学C带你飞)
  3. 加权平均法和移动加权法的例题
  4. 学习 canvas (二) 绘制图表
  5. 保险公司智能运营系统——软件需求规格说明
  6. @Zabbix配置snmptrap及使用snmptt解析格式化输出
  7. MATLAB简介与桌面系统
  8. sql 根据省份证号码提取年龄
  9. 阿里云实现首个云上量子加密通讯服务
  10. 分享100个好看且实用的PPT模板