Testing Unreal Engine Games: Automation Framework and Best Practices

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 -NullRHI

Latent (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 -NullRHI

Testing 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=Win64

Define 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.txt

Unreal'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.

Read more