大家好,我Jim ‘Anodoin’ Merrill,我的工作是致力于英雄联盟的自动化测试,特别关注的是游戏中的体验。我现在担任一个技术团队的负责人,致力于构建验证系统开发(BSV-Dev)团队。主要工作是构建自动化测试工具,帮助其它团队书写更棒的测试。
在过去的几年当中,我们致力于改良我们的测试系统及基础设施,来提高开发人员效率以及减少上线的bug。现在我们每天要跑10W个测试用例,这个数量的测试用例可以让内容以更少的Bug、更快的的呈现给玩家。我想来分享一点我们所做的工作,希望能开启一段游戏领域的自动化测试的交流。
为什么我们关心?
英雄联盟更新非常非常快。我们平均每天能看到100处以上代码或内容变更提交到版本控制系统,想要对所有这些修改提供充分的覆盖是一个挑战。由于每两周更新一个补丁,快速发现漏洞至关重要。如果在发布过程中bug发现的晚了可能会导致发布延迟,甚至重新部署或者需要暂时禁止某个英雄等,这些都是非常不好的玩家体验。自动化解放了我们的质量分析师(QA)使他们可以关注更有创造性的测试以及上游缺陷的预防,这里他们可以提供更多的价值。
自动化也能更快的反馈测试结果。每次代码或内容提交都人工扫一遍所有测试是非常不可行的,如果非要这么做,那需要一支测试军队来保证足够快得返回结果。
我们的测试系统在持续集成(CI)上运行而且提交后1小时内返回报告。这意味着开发人员可以在一个合理的时间内得到测试结果,这有助于减少上下文的切换;事实上,自动化发现的bug解决的速度是平均bug的8倍。更好的是,如果我们需要增加测试的吞吐量,我们可以简单的向测试场中添加执行器即可。
构建验证系统
这个富有想象力的命名——构建验证系统 (BVS) 是我们针对游戏的客户端及服务端的测试框。他负责载入测试用组件,并部署到测试机,启动并管理测试,执行测试,并报告其结果。这些测试与组件都是Python编写的,我们编写了大量的BVS代码来使测试的编写者们能从复杂的收集收集依赖的过程中解脱出来。最后,只需要测试类中的几个参数,就可以指定运行那个地图,加载多少个客户端,以及游戏在什么级别的联赛中。
测试使用远程过程调用(RPC)端点暴露在客户端及服务端,以便执行命名以及管理游戏状态。大部分情况,测试包括了一个线性的指令和查询的集合——现有的测试覆盖了从英雄技能到视觉规则到击杀小兵期望获得的奖励。我们更早的一些测试是非线性的,这对一些技术稍差的开发人员难度要大得多。
由于一个测试环境的所有的配置工作都是隔离的,不论是本地测试环境还是测试场效果都是一样的。这样当对游戏做出修改的时候就可以很方便的在本地跑测试。
例如,我们对Kog’Maw的新的W技能写了如下的测试:
""" Name: BioArcaneBarrage_DamageDealt Description: Verifies the damage modifications from Bio-Arcane Barrage Verifies: - KogMaw deals less damage to non-lane minions - KogMaw deals percentile magic damage - KogMaw deals normal damage to lane minions """ from KogMawAbilityTest import KogMawAbilityTest from Drivers.LOLGame.LOLGameUtils import Enumerations import KogMawStats class BioArcaneBarrage_DamageDealt(KogMawAbilityTest): def __init__(self, championAbilities): super(BioArcaneBarrage_DamageDealt, self).__init__(championAbilities) self.ability = 'Bio-Arcane Barrage' self.slot = KogMawStats.W_SLOT self.details = 'Kog\'Maw deals reduced base-damage to non-minions with additional percentile damage' self.playerLocation = Enumerations.SRULocations.MID_LANE self.enemyAnnieLocation = Enumerations.SRULocations.MID_LANE.angularOffsetDegrees(45, 200) self.enemyMinionLocation = Enumerations.SRULocations.MID_LANE.angularOffsetDegrees(45, 400) def setup(self): super(BioArcaneBarrage_DamageDealt, self).setup() self.enemyAnnie = self.spawnEnemyAnnie(self.enemyAnnieLocation) self.enemyMinion = self.spawnEnemyMinion(self.enemyMinionLocation) self.teleport(self.player, self.playerLocation) self.issueStopCommand(self.player) def execute(self): self.takeSnapshot('preCast') self.castSpellOnTarget(self.player, self.slot, self.player) self.champAttackOnce(self.player, self.enemyAnnie) self.takeRecentDeathRecapSnap(self.enemyAnnie, "annieRecap") self.resetCooldowns(self.player) self.castSpellOnTarget(self.player, self.slot, self.player) self.champAttackOnce(self.player, self.enemyMinion) self.takeSnapshot('minionRecap') self.teleport(self.player, Enumerations.SRULocations.ORDER_FOUNTAIN) def verify(self): # Verify that enemy Annie is taking the correct amount of damage. annieAutoDamageEvents = self.getDeathRecapEvents(self.player, "Attack", "annieRecap") annieAutoDamage = 0 for event in annieAutoDamageEvents: annieAutoDamage += event.PhysicalDamage annieSpellDamageEvents = self.getDeathRecapEvents(self.player, "Spell", "annieRecap", scriptName=KogMawStats.W_MAGIC_DAMAGE_SCRIPT_NAME) annieSpellDamage = 0 for event in annieSpellDamageEvents: annieSpellDamage = event.MagicDamage AD = self.getStat(self.player, "AttackDamageItem") expectedPercentile = (KogMawStats.W_AD_DAMAGE_RATIO * AD)/100 annieTotalHealth = self.getStat(self.enemyAnnie, "MaxHealth") expectedPercentileDamage = self.asPostResistDamage(self.enemyAnnie, expectedPercentile * annieTotalHealth, 'MagicResist', snapshot='preCast') self.assertInRange(annieSpellDamage, expectedPercentileDamage, expectedPercentileDamage * .1, "{} magic damage dealt. Expected ~{}".format(annieSpellDamage, expectedPercentileDamage)) expectedPhysicalDamage = self.asPostResistDamage(self.enemyAnnie, KogMawStats.W_NON_MINION_DAMAGE_RATIO * AD, 'Armor', snapshot='preCast') self.assertInRange(annieAutoDamage, expectedPhysicalDamage, expectedPhysicalDamage * .1, "{} physical damage dealt. Expected ~{}".format(annieAutoDamage, expectedPhysicalDamage)) # Verify that enemy minion is taking the correct amount of damage. AD = self.getStat(self.player, "AttackDamageItem") minionExpectedPhysicalDamage = self.asPostResistDamage(self.enemyMinion, AD, 'Armor', snapshot='preCast') expectedPercentile = (KogMawStats.W_AD_DAMAGE_RATIO * AD)/100 minionTotalHealth = self.getStat(self.enemyMinion, "MaxHealth") minionExpectedMagicDamage = self.asPostResistDamage(self.enemyMinion, expectedPercentile * minionTotalHealth, 'MagicResist', snapshot='preCast') expectedDamage = minionExpectedMagicDamage + minionExpectedPhysicalDamage actualDamage = self.getDamageTaken(self.enemyMinion, 'preCast', 'minionRecap') self.assertInRange(actualDamage, expectedDamage, 1, "{} total physical and magic damage dealt. Expected ~{}".format(annieAutoDamage, expectedDamage)) def teardown(self): self.destroy(self.enemyAnnie) self.destroy(self.enemyMinion) |
Kog’Maw整套测试的第一部分,包含了Arcane Barrage的伤害测试,具体过程如下: