蒼時弦也
蒼時弦也
資深軟體工程師
發表於

Unreal Engine 4 的自動化測試

最近幾年做測試似乎變成一個非常熱門的議題,而且也逐漸的被大多開發者了解到做測試的優點。不過,一般的軟體可以做測試倒是沒有什麼問題,那麼遊戲該怎麼做測試呢?

我自己認為這是一個很難探討的問題,大部份的遊戲就基於不確定性而變得有趣。在充滿不確定的情境下,要做測試就變得非常困難了。

不過,還是有像是基本的公式計算、數值檢查等等可以做基本的檢查,雖然無法完全的對遊玩上做完整的測試。但是至少可以確保功能上與數值上是以正確的數值做計算。

那麼,就來談談 Unreal Engien 4 的自動化測試工具 Automation Tools 吧!

根據官方文件的介紹,一共有五種功能包含在這個工具內。

  • Unit Test - 單元測試
  • Feature Test - 功能測試
  • Smoke Test - 快速測試(我認為是這樣翻譯)
  • Content Stress Test - (內容)壓力測試
  • Screen Shot Comparison - 截圖比較

不過我個人認為前面三種類似都很相似,而在 Unreal Engine 裡面硬要去區分似乎也是有點困難的。

像是 Unit Test 的範例,使用的是 Somke Test 的模式

撰寫測試

這部分的測試比較接近 Unit Test 的形式,主要是針對遊戲中的 API 做測試。

首先,我們要在 [Project]/Private 目錄下新增一個 Tests 目錄,預設的測試檔案都會從這個目錄偵測。

官方並沒有指出修改目錄的方式,因此也只能這樣使用。

假設我要測試的是名為 NPCTalk 的功能,我的測試檔名就命名為 NPCTalkTest.cpp 基本上跟大部份的測試工具一樣。

1// 假設有一個 NPCTalk 的 Class 實作了一個 speak() 的靜態方法
2FString NPCTalk::speak() {
3	return TEXT("Hello World!");
4}

那麼,我們的測試檔案就可以這樣寫。

 1#include "NPCTalk.h"
 2#include "AutomationTest.h"
 3
 4IMPLEMENT_SIMPLE_AUTOMATION_TEST(FNPCTalkTest, "NPC.Talk.Speak", EAutomationTestFlags::ATF_SmokeTest);
 5
 6bool FNPCTalkTest::RunTest(const FString &Parameters)
 7{
 8	TestEqual(TEXT("NPC should says: Hello World!"), NPCTalk::speak(), TEXT("Hello World!"));
 9  
10  return true;
11}

如此一來,當我們執行測試的時候,就可以檢查傳回的字串是否跟預期的一項是否為 Hello World! 了!

IMPLEMENT_SIMPLE_AUTOMATION_TEST 是預先寫好的巨集,用來幫助我們產生測試用的 Class 參數依序為:名稱、識別名稱(會在 Frontend 上顯示)、測試類型。

測試類型基本上分為:遊戲、編輯器、命令列三種,詳細的請參考 AutomationTest.h 這個檔案。

這邊強烈建議用有自動補全的編輯器,因為官方文件基本上完全都沒有解釋使用的方式。

對 Actor 做測試

這部分就比較接近功能測試,用來檢測某些物件是否正常。不過缺點就是要做這些測試通常只有兩個選項,直接在遊戲地圖測試,或者製作專用的測試地圖。

這要依照測試情境去決定,像是只是要測試某一種類的怪是否正常,卻得把場景上數百隻怪都測試一遍,除了費時之外也很不方便。

 1#include "MyActor.h"
 2
 3
 4// Sets default values
 5AMyActor::AMyActor()
 6{
 7 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
 8	PrimaryActorTick.bCanEverTick = true;
 9
10}
11
12// Called when the game starts or when spawned
13void AMyActor::BeginPlay()
14{
15	Super::BeginPlay();
16	
17}
18
19// Called every frame
20void AMyActor::Tick( float DeltaTime )
21{
22	Super::Tick( DeltaTime );
23
24}
25
26float AMyActor::CalcDamage() {
27    float randRatio = FMath::FRandRange(0.8, 1.2);
28    return 100.0f * randRatio;
29}

這邊我寫了一個簡易的 Acotr 並且時做了一個叫做 CalcDamage 的方法,隨機傳回 80 ~ 120 的傷害數值。 接下來我們會針對場景上所有為 MyActor 類型的物件做檢測。

因此,我們需要先創建一張新的地圖,並且把這些 Actor 放進場景。

螢幕快照 2015-06-12 下午4.30.22.png

如上圖所示,我在一張叫做 TestMap 的地圖中放置了三個類型是 MyActor 的物件。 接下來存擋即可,這樣我們就可以針對這三個物件做測試了!

 1#include "AutomationTest.h"
 2#include "AutomationCommon.h"
 3#include "MyActor.h"
 4
 5DECLARE_LOG_CATEGORY_EXTERN(GameTest, All, All);
 6DEFINE_LOG_CATEGORY(GameTest);
 7
 8IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMyActorTest, "MyActor.CalcDamage", EAutomationTestFlags::ATF_Game);
 9
10const float ACTOR_DAMAGE = 100.0f;
11const float MIN_DAMAGE_RATIO = 0.8f;
12const float MAX_DAMAGE_RATIO = 1.2f;
13
14bool FMyActorTest::RunTest(const FString &Parameters)
15{
16    UGameplayStatics::OpenLevel(GWorld, TEXT("TestMap"));
17    for(TObjectIterator<AMyActor> ActorIterator; ActorIterator; ++ActorIterator) {
18        AMyActor* MyActor = *ActorIterator;
19        if(MyActor) {
20            float actorDamage = MyActor->CalcDamage();
21            bool bIsDamageValid = actorDamage >= (ACTOR_DAMAGE * MIN_DAMAGE_RATIO) &&
22                                  actorDamage <= (ACTOR_DAMAGE * MAX_DAMAGE_RATIO);
23            TestTrue(TEXT("Damage should in the range 80.0 ~ 120.0"), bIsDamageValid);
24        } else {
25            UE_LOG(GameTest, Error, TEXT("No My Actor found"));
26            return false;
27        }
28    }
29    
30    return true;
31}

基本上就跟前一段的測試差不多,不過這邊將 Flag 限制為 ATF_Game 表示只有在 Standlone Mode 的時候才能夠運行這個測試(因為 Actor 必須實際在遊戲中執行才能表現出正常的運作)

就理論上來說,也許能夠做到像是自動操作玩家打怪這類型的測試。 AI 可控制的並不限定是一般的怪,玩家也屬於 Pawn 是可被 AI 控制而非玩家的輸入設備(鍵盤、搖桿等)

上述程式碼的寫法是參考 Unreal 論壇的這篇文章去實作的,也是我唯一看到比較完整關於 Automation Tools 的使用範例。

直接用 TObjectIterator 就可以拿到場景物件蠻神奇的,不過大概就跟一般遊戲執行時呼叫一樣吧 XD

螢幕快照 2015-06-12 下午4.36.36.png

因為是 Standlone Mode 才能執行的測試,我們需要借助 Unreal Frontend 去幫我們開啟遊戲。

我試過用 Command Line / Editor 去開啟,都無法正常使用,這算是最方便使用的解法了! Mac/Windows 都在引擎的 Binaries 目錄下,稍微找一下就可以找到了(一般放在對應的作業系統目錄中)

螢幕快照 2015-06-12 下午4.38.35.png

進去 Project Launcher 分頁,先把左上角的 Project 設定成要測試的專案,然後按下 Launch 就會開啟一個獨立的遊戲出來。

螢幕快照 2015-06-12 下午4.40.15.png

開啟遊戲視窗後,進到 Session Frontend 裡面,選下面的 Automation 分頁,再點選 Find Workers 確保有抓到目前要測試的遊戲。

我的截圖有一個 Instance 是 Timeout 狀態,那是因為我上一次跑完關掉後就會呈現這樣的狀態(無法清除掉蠻痛苦的⋯⋯)

螢幕快照 2015-06-12 下午4.42.18.png

勾選好要跑的測試之後(預設只會跑幾個比較快的,一般是 Somke Test)讓他執行,如果順利通過就會像我這樣顯示綠色的。 同時也會發現遊戲地圖會切換到最後一個跑的測試地圖上,簡單說要做這類的測試會很花時間(苦笑)

可以看到功能測試佔用的時間很長,搭配持續整合工具(Jenkins)之類的可能可以解決這個問題。 昨天參加 Epic Games 的演講,官方人員告訴我是可行的。不過跟自動化測試一樣,很少人討論(折衷方案是利用區網空閒的電腦,丟 Frontend 去跑,但是作業系統不同的關係我無法測試這個方法)

小結

目前測試這個功能就到這邊,大致上不難應用。稍微熟悉的話至少能幫遊戲做一部分的測試,我想之後應該能節省掉不少時間吧!

雖然還有像是開發 Plugin 時能不能做測試等等的問題,以及整合 CI 後該怎麼跑,不過就現況來說已經算是讓人挺滿意了!

另外就是 Smoke Test 在 Editor 就可以用裡面的 Frontend 跑,不用特地單獨開 Frontend 來跑。