quodsoler/unreal-engine-skills

ue-testing-debugging

Use when writing automation tests, functional tests, or any test in Unreal Engine.

Ver código fuente
Documento original del Skill

Contenido del repositorio de origen con títulos, ejemplos, código, tablas, enlaces e imágenes preservados.

UE Testing & Debugging

You are an expert in testing, debugging, and profiling Unreal Engine C++ projects.

Context

Read .agents/ue-project-context.md for engine version, existing log categories, test infrastructure (automation modules, test maps), and project-specific conventions before providing guidance.

Before You Start

Ask which area the user needs help with if unclear:

  • Automation tests — unit/integration tests using IMPLEMENTSIMPLEAUTOMATION_TEST
  • Functional tests — actor-based AFunctionalTest in maps
  • Logging — UE_LOG, custom categories, verbosity filtering
  • Assertions — check, ensure, verify and when to use each
  • Debug drawing — DrawDebug helpers for runtime visualization
  • Console commands — UFUNCTION(Exec), FAutoConsoleCommand, CVars
  • Profiling — Unreal Insights, stat commands, SCOPECYCLECOUNTER

Automation Framework

Automation tests live in a dedicated module (e.g., MyGameTests) that depends on "AutomationController". Include the module in the editor target via ExtraModuleNames and conditionally in the game target via if (bWithAutomationTests).

Simple Tests

cpp
// Source/MyGameTests/Private/MyFeature.spec.cpp
#include "Misc/AutomationTest.h"
// Must specify one application context flag AND exactly one filter flag
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMyInventoryTest, "MyGame.Inventory.AddItem",
    EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter)

bool FMyInventoryTest::RunTest(const FString& Parameters)
{
    UInventoryComponent* Inv = NewObject<UInventoryComponent>();
    Inv->AddItem(FName("Sword"), 1);
    TestEqual(TEXT("Item count after add"), Inv->GetItemCount(FName("Sword")), 1);
    TestTrue(TEXT("Has sword"), Inv->HasItem(FName("Sword")));
    TestFalse(TEXT("No axe"),   Inv->HasItem(FName("Axe")));
    TestNotNull(TEXT("Inv valid"), Inv);
    return true;
}

Complex / Parameterized Tests

IMPLEMENT_COMPLEX_AUTOMATION_TEST requires overriding GetTests() to populate the parameter list, and RunTest(Parameters) receives each entry in turn.

cpp
IMPLEMENT_COMPLEX_AUTOMATION_TEST(FMyAssetLoadTest, "MyGame.Assets.LoadByPath",
    EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter)

void FMyAssetLoadTest::GetTests(TArray<FString>& OutBeautifiedNames,
                                 TArray<FString>& OutTestCommands) const
{
    OutBeautifiedNames.Add(TEXT("Sword"));  OutTestCommands.Add(TEXT("/Game/Items/BP_Sword"));
    OutBeautifiedNames.Add(TEXT("Shield")); OutTestCommands.Add(TEXT("/Game/Items/BP_Shield"));
}

bool FMyAssetLoadTest::RunTest(const FString& Parameters)
{
    UObject* Asset = StaticLoadObject(UObject::StaticClass(), nullptr, *Parameters);
    if (!TestNotNull(TEXT("Asset loaded"), Asset)) { return false; }
    TestTrue(TEXT("Is DataAsset"), Asset->IsA<UPrimaryDataAsset>());
    return true;
}

Test Assertion Methods

cpp
// Equality — overloaded for int32, int64, float, double, FVector, FRotator,
//            FTransform, FColor, FLinearColor, FString, FStringView, FText, FName
TestEqual(TEXT("Label"), Actual, Expected);
TestEqual(TEXT("Float approx"), ActualF, ExpectedF, 0.001f);   // tolerance overload

// Boolean
TestTrue(TEXT("Label"), bCondition);
TestFalse(TEXT("Label"), bCondition);

// Pointer / validity
TestNotNull(TEXT("Label"), Ptr);          // fails if Ptr == nullptr
TestNull(TEXT("Label"), Ptr);             // fails if Ptr != nullptr
TestValid(TEXT("Label"), WeakPtr);        // IsValid() check on TWeakObjectPtr etc.

// Custom failure messages
AddError(FString::Printf(TEXT("Unexpected damage %d"), Damage));
AddWarning(TEXT("Deprecated path hit"));

// Add error and bail — returns false from RunTest if condition fails
UE_RETURN_ON_ERROR(Ptr != nullptr, TEXT("Ptr must not be null"));

Test Flags Reference

FlagMeaning
EditorContextRuns in the editor process
ClientContextRuns in game client
ServerContextRuns on dedicated server
SmokeFilterFast; runs on every CI check-in
ProductFilterProject/game-level tests

Latent Commands (Async Testing)

Use latent commands when the test must wait for an async operation. Update() returns true when done, false to retry next frame.

cpp
DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FWaitSecondsCommand, float, Duration);
bool FWaitSecondsCommand::Update() { return GetCurrentRunTime() >= Duration; }

// Enqueue inside RunTest; commands drain sequentially after RunTest returns
ADD_LATENT_AUTOMATION_COMMAND(FWaitSecondsCommand(2.0f));

See references/automation-test-patterns.md for delegate-wait, timeout, and post-async assertion patterns.


Functional Tests

AFunctionalTest is a UCLASS actor placed in a test map. Override StartTest() and call FinishTest() when done.

cpp
// MyFunctionalTest.h
UCLASS()
class MYGAMETESTS_API AMyFunctionalTest : public AFunctionalTest
{
    GENERATED_BODY()
public:
    virtual void StartTest() override;
private:
    UFUNCTION() void OnTimerExpired();
    FTimerHandle TimerHandle;
};

// MyFunctionalTest.cpp
void AMyFunctionalTest::StartTest()
{
    Super::StartTest();
    GetWorldTimerManager().SetTimer(TimerHandle, this,
        &AMyFunctionalTest::OnTimerExpired, 1.0f, false);
}

void AMyFunctionalTest::OnTimerExpired()
{
    bool bPassed = /* verify world state */ true;
    FinishTest(bPassed ? EFunctionalTestResult::Succeeded
                       : EFunctionalTestResult::Failed,
               TEXT("Timer-based check"));
}

Place actors in Maps/Test_MyFeature.umap. Run via RunAutomationTest "MyGame.Functional.MyFeature" or the Session Frontend.

TimeLimit

cpp
// Timeout — auto-fail if test exceeds time limit
void AMyFunctionalTest::PrepareTest()
{
    Super::PrepareTest();
    TimeLimit = 10.0f;  // seconds; 0 = no timeout (default)
}

Logging

Declaring and Defining a Category

cpp
// MyModule.h  — visible across the module
DECLARE_LOG_CATEGORY_EXTERN(LogMyModule, Log, All);
//                           ^Name       ^Default  ^CompileTime max

// MyModule.cpp
DEFINE_LOG_CATEGORY(LogMyModule);

// Single-file static category (no extern needed)
DEFINE_LOG_CATEGORY_STATIC(LogMyHelper, Warning, All);

UE_LOG Usage

cpp
// UE_LOG(CategoryName, Verbosity, Format, ...)
UE_LOG(LogMyModule, Log,        TEXT("Player spawned: %s"), *PlayerName);
UE_LOG(LogMyModule, Warning,    TEXT("Inventory full, dropping item %s"), *ItemName);
UE_LOG(LogMyModule, Error,      TEXT("Failed to load asset at %s"), *AssetPath);
UE_LOG(LogMyModule, Verbose,    TEXT("Tick called, dt=%.4f"), DeltaTime);
UE_LOG(LogMyModule, Fatal,      TEXT("Unrecoverable save corruption")); // crashes

// Conditional log — condition evaluated only if category/verbosity is active
UE_CLOG(Health <= 0.f, LogMyModule, Warning, TEXT("Actor %s has zero health"), *GetName());

Verbosity Levels (highest to lowest severity)

LevelWhen to use
FatalCrash-worthy unrecoverable errors
ErrorOperation failed, needs developer attention
WarningUnexpected but recoverable condition
DisplayUser-visible output (always shown)
LogStandard development info
VerboseDetailed per-frame or per-call info
VeryVerboseTrace-level; very high frequency

Structured Logging (UE 5.2+)

cpp
#include "Logging/StructuredLog.h"
// Named fields — order does not matter; extra fields beyond the format string are allowed
UE_LOGFMT(LogMyModule, Warning,
    "Loading '{Name}' failed with error {Error}",
    ("Name", AssetName), ("Error", ErrorCode), ("Flags", LoadFlags));

Runtime Log Filtering

# Command line: set a category to Verbose at startup
-LogCmds="LogMyModule Verbose, LogAI Warning"

# Console at runtime
Log LogMyModule Verbose
Log LogAI Warning
Log reset          # restore defaults

Assertions

Assertions are defined in Misc/AssertionMacros.h. Understand the build-configuration behaviour before choosing one.

check / checkf

cpp
// Active in Debug, Development, Test. REMOVED in Shipping (expression not evaluated).
check(Ptr != nullptr);
checkf(Index >= 0 && Index < Array.Num(),
       TEXT("Index %d out of range [0,%d)"), Index, Array.Num());

// Debug-only (DO_GUARD_SLOW — only Debug builds)
checkSlow(ExpensiveValidationFn());

// Marks unreachable code paths
checkNoEntry();

// Marks code that must only execute once
checkNoReentry();

ensure / ensureMsgf

cpp
// Non-fatal. Logs callstack and submits crash report. Execution continues.
// Fires only ONCE per call site per session (subsequent failures are silent).
if (ensure(Component != nullptr))
{
    Component->DoWork();  // safe to call here
}

// With a message
ensureMsgf(Health > 0.f, TEXT("Actor %s has non-positive health %.1f"),
           *GetName(), Health);

// Always fires (not just once per session)
ensureAlways(bInitialized);
ensureAlwaysMsgf(bInitialized, TEXT("System not initialized before use"));

verify / verifyf

cpp
// Expression ALWAYS evaluated (even in Shipping), but only halts in non-Shipping.
// Use when expression has side effects you always need.
verify(Manager->Init());
verifyf(Count++ < MaxCount, TEXT("Exceeded max count %d"), MaxCount);

Decision Guide

SituationMacro
Class invariant, programmer errorcheck / checkf
Recoverable condition, want to continueensure / ensureMsgf
Expression has side effects always neededverify / verifyf
Debug-build heavy validationcheckSlow
User-facing input validationnone — use explicit if/return

Debug Drawing

Debug draw functions from DrawDebugHelpers.h render geometry directly in the world viewport during PIE or standalone builds. They are stripped by #if ENABLE_DRAW_DEBUG in Shipping.

cpp
#include "DrawDebugHelpers.h"

// Duration > 0 = seconds visible; bPersistentLines = true for permanent.
// Duration 0 or -1 both show for one frame.
UWorld* World = GetWorld();
DrawDebugLine(World, StartLoc, EndLoc, FColor::Red, false, 2.0f, 0, 2.0f);
DrawDebugSphere(World, Center, Radius, 12, FColor::Green, false, 2.0f);
DrawDebugBox(World, Center, Extent, FColor::Blue, false, 2.0f);
DrawDebugCapsule(World, Center, HalfHeight, Radius,
                 FQuat::Identity, FColor::Yellow, false, 2.0f);
DrawDebugPoint(World, Location, 8.0f, FColor::White, false, 2.0f);
DrawDebugDirectionalArrow(World, Start, End, 40.0f, FColor::Cyan, false, 2.0f);
DrawDebugString(World, Location, TEXT("Label"), nullptr,
                FColor::White, 2.0f, true);

// Clear all persistent debug geometry
FlushPersistentDebugLines(World);

// Guard persistent draws for shipping: #if ENABLE_DRAW_DEBUG ... #endif
// UKismetSystemLibrary::DrawDebugSphere (and siblings) auto-gate behind ENABLE_DRAW_DEBUG

Console Commands

Exec Functions

cpp
// AMyPlayerController.h
UFUNCTION(Exec)
void ToggleGodMode();

// AMyPlayerController.cpp
void AMyPlayerController::ToggleGodMode()
{
    bGodMode = !bGodMode;
    UE_LOG(LogMyModule, Display, TEXT("GodMode: %s"), bGodMode ? TEXT("ON") : TEXT("OFF"));
}

Exec functions work when typed in the console (~) if on a PlayerController, Pawn, HUD, GameMode, GameState, CheatManager, or GameInstance.

FAutoConsoleCommand and CVars

cpp
// Registers at static init; no world context needed
static FAutoConsoleCommand GDumpStatsCmd(
    TEXT("MyGame.DumpStats"), TEXT("Dump gameplay stats"),
    FConsoleCommandDelegate::CreateLambda([]()
    {
        UE_LOG(LogMyModule, Display, TEXT("=== GameStats ==="));
    }));

// With world + args (PIE-safe; prefer this for gameplay commands)
static FAutoConsoleCommandWithWorldAndArgs GSpawnItemCmd(
    TEXT("MyGame.SpawnItem"), TEXT("Usage: MyGame.SpawnItem <Name>"),
    FConsoleCommandWithWorldAndArgsDelegate::CreateLambda(
        [](const TArray<FString>& Args, UWorld* World) { /* spawn */ }));

// CVar — declare in .cpp to avoid ODR issues
static TAutoConsoleVariable<float> CVarDamageScale(
    TEXT("MyGame.DamageScale"), 1.0f,
    TEXT("Scales all outgoing damage."), ECVF_Cheat);
float Scale = CVarDamageScale.GetValueOnGameThread();

// Attach CVar to an existing variable
float GDrawDistance = 5000.0f;
static FAutoConsoleVariableRef CVarDrawDistance(
    TEXT("MyGame.DrawDistance"), GDrawDistance,
    TEXT("Draw distance for gameplay objects"), ECVF_Default);

Runtime Command Registration

cpp
// Runtime registration — call from StartupModule, Initialize, or BeginPlay
IConsoleCommand* Cmd = IConsoleManager::Get().RegisterConsoleCommand(
    TEXT("MyGame.ReloadConfig"),
    TEXT("Reloads runtime config"),
    FConsoleCommandDelegate::CreateUObject(this, &UMySubsystem::ReloadConfig));
// Unregister when the owning object is destroyed:
IConsoleManager::Get().UnregisterConsoleObject(Cmd);

Unlike FAutoConsoleCommand (static init registration), RegisterConsoleCommand registers at runtime and returns a handle for explicit cleanup.


Custom Stat Groups & Profiling Markers

cpp
// Declare in a .cpp — feeds stat commands and Unreal Insights
DECLARE_STATS_GROUP(TEXT("MyGame"), STATGROUP_MyGame, STATCAT_Advanced);
DECLARE_CYCLE_STAT(TEXT("MySystem Tick"), STAT_MySystemTick, STATGROUP_MyGame);
DECLARE_CYCLE_STAT(TEXT("PathFind"),      STAT_PathFind,     STATGROUP_MyGame);

void UMySystem::Tick(float DeltaTime) { SCOPE_CYCLE_COUNTER(STAT_MySystemTick); }
FPath UMySystem::FindPath(...)        { SCOPE_CYCLE_COUNTER(STAT_PathFind); }

// Named events — colored blocks in Insights, no stat overhead
#include "HAL/PlatformMisc.h"
void UMySystem::HeavyOperation() { SCOPED_NAMED_EVENT(MyHeavyOp, FColor::Orange); }

// CSV profiling — lightweight always-on telemetry written to Saved/Profiling/CSVStats/
#include "ProfilingDebugging/CsvProfiler.h"
CSV_DEFINE_CATEGORY(MyGame, true);
void UMySystem::Tick(float DeltaTime) { CSV_SCOPED_TIMING_STAT(MyGame, SystemTick); }

Debugging Techniques

Visual Logger

cpp
#include "VisualLogger/VisualLogger.h"
// Window > Visual Logger shows a timeline of shapes and log events per actor
UE_VLOG_SPHERE(this, LogMyModule, Verbose, GetActorLocation(), 50.0f, FColor::Green, TEXT("Patrol point"));
UE_VLOG_SEGMENT(this, LogMyModule, Log, From, To, FColor::Red, TEXT("Path"));
UE_VLOG(this, LogMyModule, Log, TEXT("State: %s"), *StateName);

Gameplay Debugger

Press ' (apostrophe) in PIE to open the Gameplay Debugger. It shows AI, EQS, Ability System, and custom categories. Register a custom category in module startup:

cpp
IGameplayDebugger::Get().RegisterCategory("MySystem",
    IGameplayDebugger::FOnGetCategory::CreateStatic(&FMyDebugCategory::MakeInstance),
    EGameplayDebuggerCategoryState::EnabledInGame);

IDE Breakpoint Debugging

Attach Visual Studio or Rider to the running UnrealEditor process (Debug > Attach to Process). Use DebugGame or Debug configuration for full symbol resolution -- Development strips many symbols.

cpp
// Break on assertion failures -- set breakpoints on these functions:
// FDebug::AssertFailed       (check/checkf)
// FDebug::EnsureFailed       (ensure/ensureMsgf)
// FDebug::OptionallyLogFormattedEnsureMessageReturningFalse

// Conditional breakpoint example (VS/Rider):
// Condition: Actor->GetName().Contains(TEXT("Enemy"))
// Hit count: break on 5th hit

For check() and ensure() failures, set breakpoints on the handler functions above -- they fire before crash/log, letting you inspect the call stack.

Crash Analysis

  • Minidumps land in Saved/Crashes/. Open with UnrealEditor-Win64-DebugGame PDB in WinDbg or Rider.
  • FDebug::DumpStackTraceToLog(ELogVerbosity::Error) prints the current callstack to the log.
  • ensure submits a callstack to the Crash Reporter without crashing the process.

Network: -NetTrace for replication capture, stat net for live bandwidth, net.ListActorChannels for actor channels.

Key profiling commands: stat startfile/stat stopfile (.uestats capture for Insights), stat gpu/ProfileGPU (GPU timing), stat memoryplatform/memreport -full (memory), -trace=cpu,gpu,frame,memory (Insights launch args). See references/profiling-commands.md.


Common Mistakes

check() in shippingcheck() expressions are compiled out of Shipping builds. Never put required logic inside a check expression; use verify() if the expression must always evaluate.

ensure fires only once — After the first ensure failure at a call site, subsequent calls at that site are silent. Use ensureAlways if you need every failure reported.

DrawDebug in shipping — DrawDebug calls do not exist in Shipping without ENABLE_DRAW_DEBUG. Wrap persistent draws with #if ENABLE_DRAW_DEBUG.

Missing log category — Defining UE_LOG with a category not visible in the current translation unit causes a linker error. Include the header that declares the category.

Automation test without a filter flag — Every IMPLEMENT_SIMPLE_AUTOMATION_TEST must have exactly one filter flag (Smoke, Engine, Product, Perf, Stress, or Negative). Missing it is a compile-time static_assert failure.

Latent command after `return true` — Latent commands are enqueued before the function returns. Do not enqueue them after the return true; statement.

Log spam in multiplayer — Identical log calls fire from both server and each client. Prefix messages with GetWorld()->GetNetMode() or use UE_CLOG(HasAuthority(), ...) to reduce noise.


Related Skills

  • ue-cpp-foundations — UE macro system, delegates, FString, UE_LOG basics
  • ue-module-build-system — setting up a dedicated test module and target inclusion
  • ue-actor-component-architecture — AFunctionalTest placement and world interaction

References

  • references/automation-test-patterns.md — test setup patterns, latent commands, common scenarios
  • references/profiling-commands.md — stat commands, Insights capture, analysis workflow
del mismo repositorio

Más Skills

Todos los Skills
quodsoler
Comunidad

ue-async-threading

Use this skill when working with Unreal Engine async operations, threading, parallel execution, or concurrency. Also use when the user mentions 'FRunnable', 'FAsyncTask', 'TaskGraph', 'UE::Tasks', 'ParallelFor', 'TFuture', 'TPromise', 'Async()', 'thread safety', 'FCriticalSection', 'FRWLock', 'background thread', 'game thread dispatch', or 'thread pool'. For networking async (RPCs, replication), see ue-networking-replication. For asset streaming, see ue-data-assets-tables.

instalaciones
2
GitHub Stars
335
Actualizado
1 mar
quodsoler
Comunidad

ue-cpp-foundations

Use when writing Unreal Engine C++ code involving UPROPERTY, UFUNCTION, UCLASS, TArray, TMap, delegates, FString, garbage collection, or smart pointers. Also use when the user asks about "UE C++", USTRUCT, UENUM, FName, FText, TObjectPtr, TWeakObjectPtr, UObject lifetime, UELOG, or UE subsystems. For module build configuration, see ue-module-build-system. For Actor/Component architecture, see ue-actor-component-architecture.

instalaciones
2
GitHub Stars
335
Actualizado
1 mar
quodsoler
Comunidad

ue-data-assets-tables

Use when working with DataAsset, DataTable, soft reference, hard reference, TSoftObjectPtr, async loading, Asset Manager, StreamableManager, or game data structures in Unreal Engine. See references/asset-loading-patterns.md for async loading and StreamableManager patterns. See references/data-driven-design.md for data-driven gameplay architecture. For serialization, see ue-serialization-savegames. For C++ foundations, see ue-cpp-foundations.

instalaciones
2
GitHub Stars
335
Actualizado
1 mar
quodsoler
Comunidad

ue-editor-tools

Use when extending the Unreal Editor with editor tool, editor utility widget, Blutility, detail customization, property customization, editor mode, asset editor, editor subsystem, editor extension, UToolMenus, or scripted asset operations. For Slate fundamentals, see ue-ui-umg-slate. For module build config, see ue-module-build-system.

instalaciones
2
GitHub Stars
335
Actualizado
1 mar