[譯]揭秘英雄聯盟的自動化測試

lill7921 8年前發布 | 19K 次閱讀 自動化測試 游戲開發

來自: http://www.jianshu.com/p/f5435a91dd3e

原文: AUTOMATED TESTING FOR LEAGUE OF LEGENDS

作者:

譯者: 杰微刊兼職譯者繆晨

大家好,我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的傷害測試,具體過程如下:

當一個測試運行完,它將測試的結果提供給一個獨立的報告服務,那里存儲了過去六個月的運行數據。基于測試數據,這個服務采取不同的行動。一個本地運行的測試會在測試執行的機器上打開一個web頁面,包括通過的與失敗的測試用例的詳情。如果在測試場運行,當有任何測試用例失敗的時候,系統會自動根據測試結果創建bug標簽及issue等,并給提交者發送郵件。測試數據也會通過報告服務聚合及跟蹤,使我們可以知道什么情況下測試不通過,以及失敗的頻率,還有bug存在多久了。

在Wood 5級的比賽中我們不使用守衛,因此在我看來這個嚴重故障也沒什么問題

出于防止古怪及不可靠測試的目的,每個測試都必須經過一個標準的流程來保證可信。當一個測試經過了代碼審查并提交,它加入一個測試集合叫做BVSStaging。在那里測試在提交運行前必須穩定運行至少一周。如果在Staging中的測試失敗了,只會通知測試的開發者,來避免困惑。

當一個測試被證明可靠后,它被提交進兩個集合中的一個。第一個集合,BVSBlocker,包括的測試指出該構建是否值得進一步測試。如果一個構建在Blocker集合測試不通過是不會部署到測試環境的,因為游戲不能開始,或者有好幾個會導致服務端崩潰的bug影響游戲。相對的,BVS Core,使我們功能測試的核心集合,包括對每個英雄技能的測試。

框架深度游覽

BVS分三個層面實現:執行器,驅動及腳本。執行器為功能測試實現了一套通用的API,驅動實現的一個測試的配置與執行的具體步驟。最后,腳本實現測試用例的具體邏輯。當下,我們只有一個驅動在使用(LOL游戲),但是執行器與驅動分離的設計意味著在將來的項目里可以通過實現各自的驅動來使用BVS系統,而且可以共享使用LOL的驅動編寫時編寫的工具。

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

個別的組件注冊了他們必選和可選的參數,作為它們聲明的一部分。當命令行提供了參數,參數被作為字典存儲下來,然后組件會在初始化時處理這些參數。BVS早期的版本使用Python標準的argparse庫,但是出于兩個原因,我們選擇放棄argparse庫:第一,潛在參數的數量變得非常巨大,通過系統跟蹤變得非常困難;第二,驅動需要有一些驅動特有的參數,這意味著在啟動時聲明一個解析器是不可行的。

classTestFactory(API.TestFactoryAPI):requiredArgs = [ArgsObject('driver','Driver you wish to use'),

ArgsObject('name','Name of the test to run')]

optionalArgs = [ArgsObject('overrideConfig','Use a non-standard game.cfg',None),

ArgsObject('gameMetadataConfiguration','A string identifying which game metadata to use',None),

ArgsObject('listener','Log listener to use',None),

ArgsObject('mutator','A string name for mutator to apply to test object',None),

ArgsObject('testInfoID','Test and metadata this test run is related to',None),

ArgsObject('testSubsetNumber','The number out of total if test is subsectable',None),

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,導致一大批脆弱的測試在他們各自執行的硬件上表現完全不同。所有標準類庫中的等待操作都是條件等待,都是在等待游戲中一個特定的條件。

<p>@annotate("Wait until a unit drops the specified buff.",arguments=[argument("unitNameOrID","Unit name (or unique integer unit ID).", (str, int)), </p>

argument("buff","Buff you want to drop.", str),

argument("timeout","How long to wait.", float, default=STANDARD_TIMEOUT),

argument("interval","How often to check for a change.", float, default=SERVER_TICK),

argument('speedUp','Whether to speed the game up.', bool, default=False)],

tags=["wait","buff","change"])defwaitForBuffLost(self, unitNameOrID, buff, timeout=STANDARD_TIMEOUT, interval=SERVER_TICK, speedUp=False):conditionFunction =lambda:notself.hasBuff(unitNameOrID, buff)returnself.__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的第一發現者。我個人的得意之作發現一個缺陷:游戲中所有的塔都緩慢移向地圖的右上角,結果紫色基地的圣塔附近就出現了交通擁堵。我們的發現也包括一些非自動化測試可能無法發現的東西,比如曾經有個問題是點射技能可能會穿過一個敵人,如果英雄恰好攻擊了敵人空白點的范圍。

總體來說,自動化測試代替手工測試并不是必要的,但是它幫助我們加快了開發的反饋循環而且解放了更多的手動測試人員使他們可以關注更有害的問題的測試。隨著英雄聯盟內容的增加,我們持續增加更多的覆蓋,這樣可以提高我們的對缺陷的命中率,也讓我們對構建的健康程度更有信心。

謝謝你花時間來閱讀這篇文章,如果有問題可以在評論中直接提問。我們下一篇關于自動化測試的文章中將會討論測試的吞吐量及返回的速度。

更多精彩內容~

</div>

 本文由用戶 lill7921 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!