Testing Unreal Engine Games: Automation Framework and Best Practices
Unreal Engine ships with a built-in Automation Testing Framework that supports unit tests, functional tests, and performance testing at scale. Unlike Unity's external testing ecosystem, Unreal's automation is deeply integrated with the engine and supports testing both C++ game logic and Blueprint assets.
Unreal Automation Testing Framework
Unreal's automation system has three levels:
Unit tests — Test C++ functions in isolation. No world required. Fast.
Functional tests — Level-based tests that run in actual game worlds. Actors, physics, and all engine systems are active.
Gauntlet — Large-scale test orchestration across multiple instances and platforms. Used for performance, stress, and multiplayer testing.
Writing Unit Tests
// Source/YourGame/Tests/PlayerStatsTests.cpp
#include "Misc/AutomationTest.h"
#include "YourGame/Player/PlayerStatsComponent.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FPlayerStatsDamageTest,
"YourGame.Unit.PlayerStats.DamageReducedByArmor",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FPlayerStatsDamageTest::RunTest(const FString& Parameters)
{
UPlayerStatsComponent* Stats = NewObject<UPlayerStatsComponent>();
Stats->MaxHealth = 100.0f;
Stats->Armor = 10.0f;
Stats->CurrentHealth = 100.0f;
Stats->TakeDamage(50.0f);
// 50 raw - 10 armor = 40 damage → 60 HP remaining
TestEqual(TEXT("Health after armored damage"), Stats->CurrentHealth, 60.0f);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FPlayerStatsDeathTest,
"YourGame.Unit.PlayerStats.DeathOnFatalDamage",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FPlayerStatsDeathTest::RunTest(const FString& Parameters)
{
UPlayerStatsComponent* Stats = NewObject<UPlayerStatsComponent>();
Stats->MaxHealth = 100.0f;
Stats->Armor = 0.0f;
Stats->CurrentHealth = 100.0f;
Stats->TakeDamage(9999.0f);
TestEqual(TEXT("Health cannot go below zero"), Stats->CurrentHealth, 0.0f);
TestTrue(TEXT("Player is dead"), Stats->bIsDead);
return true;
}Run unit tests:
# Run all tests matching the filter
UnrealEditor-cmd YourProject.uproject -ExecCmds=<span class="hljs-string">"Automation RunTests YourGame.Unit" -Unattended -NullRHILatent (Async) Tests
Many game behaviors are asynchronous. Unreal's latent automation framework handles this:
IMPLEMENT_COMPLEX_AUTOMATION_TEST(
FRespawnTest,
"YourGame.Functional.Player.RespawnAfterDeath",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
void FRespawnTest::GetTests(TArray<FString>& OutBeautifiedNames, TArray<FString>& OutTestCommands) const
{
OutBeautifiedNames.Add(TEXT("Player respawns within 5 seconds"));
OutTestCommands.Add(TEXT(""));
}
bool FRespawnTest::RunTest(const FString& Parameters)
{
// Set up test world
ADD_LATENT_AUTOMATION_COMMAND(FEngineWaitLatentCommand(0.1f)); // Wait for world init
ADD_LATENT_AUTOMATION_COMMAND(FFunctionLatentCommand([this]()
{
// Kill the player
APlayerCharacter* Player = GetPlayerCharacter();
Player->GetStatsComponent()->TakeDamage(9999.0f);
return true; // Command complete
}));
// Wait up to 5 seconds for respawn
ADD_LATENT_AUTOMATION_COMMAND(FEngineWaitLatentCommand(5.0f));
ADD_LATENT_AUTOMATION_COMMAND(FFunctionLatentCommand([this]()
{
APlayerCharacter* Player = GetPlayerCharacter();
TestTrue(TEXT("Player should be alive after respawn"), Player->IsAlive());
TestEqual(TEXT("Player should have full health after respawn"),
Player->GetStatsComponent()->CurrentHealth,
Player->GetStatsComponent()->MaxHealth);
return true;
}));
return true;
}Functional Tests (Level-Based)
Functional tests run inside Unreal levels. Create a test map with actors positioned for the test scenario:
// AFunctionalTestBase subclass
UCLASS()
class AEnemyPatrolTest : public AFunctionalTest
{
GENERATED_BODY()
public:
virtual void StartTest() override;
private:
UPROPERTY(EditAnywhere)
AEnemyPatrolCharacter* PatrolEnemy;
UPROPERTY(EditAnywhere)
TArray<AActor*> PatrolPoints;
int32 VisitedPoints = 0;
void OnPatrolPointReached(AActor* PatrolPoint);
};
void AEnemyPatrolTest::StartTest()
{
PatrolEnemy->OnPatrolPointReached.AddDynamic(this, &AEnemyPatrolTest::OnPatrolPointReached);
PatrolEnemy->StartPatrol(PatrolPoints);
// Set test timeout
SetTimeLimit(30.0f, TEXT("Enemy did not complete patrol within 30 seconds"));
}
void AEnemyPatrolTest::OnPatrolPointReached(AActor* PatrolPoint)
{
VisitedPoints++;
if (VisitedPoints >= PatrolPoints.Num())
{
FinishTest(EFunctionalTestResult::Succeeded, TEXT("Enemy visited all patrol points"));
}
}Place the AEnemyPatrolTest actor in a test level with the patrol points. Run functional tests:
UnrealEditor-cmd YourProject.uproject Maps/TestMaps/EnemyPatrolTest \
-ExecCmds="Automation RunTests YourGame.Functional.Enemy.Patrol" \
-Unattended -NullRHITesting Blueprint Logic
Blueprint functions can be tested via C++ by calling them programmatically:
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FBlueprintDamageCalculationTest,
"YourGame.Blueprint.DamageSystem.CalculateDamage",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FBlueprintDamageCalculationTest::RunTest(const FString& Parameters)
{
// Load the Blueprint class
UClass* BPClass = LoadClass<UObject>(
nullptr,
TEXT("/Game/Characters/BP_DamageCalculator.BP_DamageCalculator_C")
);
if (!TestNotNull(TEXT("DamageCalculator Blueprint loaded"), BPClass))
return false;
UObject* Calculator = NewObject<UObject>(GetTransientPackage(), BPClass);
// Call Blueprint function via UFunctionCalling
FOutputDeviceNull Ar;
Calculator->CallFunctionByNameWithArguments(
TEXT("CalculateDamage 50 10"),
Ar, nullptr, true
);
// Verify result via a getter function
float Result = 0.0f;
FProperty* ResultProp = BPClass->FindPropertyByName(TEXT("LastDamageResult"));
if (ResultProp)
{
ResultProp->GetValue_InContainer(Calculator, &Result);
}
TestEqual(TEXT("CalculateDamage(50, 10) should return 40"), Result, 40.0f);
return true;
}Gauntlet for Scale Testing
Gauntlet orchestrates tests across multiple clients and servers — essential for multiplayer testing:
# Run a Gauntlet test session
RunUAT.bat RunGauntlet \
-project=YourProject \
-platform=Win64 \
-configuration=Development \
-<span class="hljs-built_in">test=YourGame.Multiplayer.LobbyTest \
-numclients=4 \
-serverplatform=Win64Define a Gauntlet test node:
// YourProject/Build/Scripts/GauntletTests/LobbyTest.cs
public class LobbyTest : UnrealTestNode<UnrealTestConfiguration>
{
public override string MaxDuration => "00:05:00";
public override ITestReport CreateReport(TestResult Result, UnrealTestContext Ctx,
UnrealBuildSource Build, IEnumerable<UnrealRoleResult> Artifacts,
string ArtifactPath)
{
UnrealRoleResult Server = Artifacts.First(A => A.Role.RoleType == UnrealTargetRole.Server);
// Parse server log for test results
var ResultLines = Server.LogSummary.Where(L => L.Contains("[TEST]")).ToList();
bool AllPassed = ResultLines.All(L => L.Contains("[PASS]"));
return new SimpleTestReport(AllPassed ? TestResult.Passed : TestResult.Failed,
$"{ResultLines.Count} tests, {ResultLines.Count(L => L.Contains("[PASS]"))} passed");
}
}CI Integration
# .github/workflows/unreal-tests.yml
name: Unreal Engine Tests
on: [push, pull_request]
jobs:
unit-tests:
runs-on: [self-hosted, windows, unreal] # Requires UE installation
steps:
- uses: actions/checkout@v4
with:
lfs: true
- name: Run unit tests
run: |
& "$env:UE_PATH\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" `
"$env:GITHUB_WORKSPACE\YourProject.uproject" `
-ExecCmds="Automation RunTests YourGame.Unit;Quit" `
-Unattended `
-NullRHI `
-log="TestLog.txt"
shell: pwsh
- name: Parse test results
run: |
$log = Get-Content TestLog.txt
$failures = $log | Where-Object { $_ -match "FAIL" }
if ($failures.Count -gt 0) {
Write-Error "Test failures found: $failures"
exit 1
}
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-logs
path: TestLog.txtUnreal's automation framework is powerful but requires C++ knowledge to use well. The investment pays off in large codebases where regression testing catches bugs that manual QA misses. Start with the highest-value unit tests for damage calculations, game mechanics, and save/load systems before adding the overhead of functional and Gauntlet tests.