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 IMPLEMENT_SIMPLE_AUTOMATION_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, SCOPE_CYCLE_COUNTER
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
// 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.
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
// 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
| Flag | Meaning |
|---|---|
EditorContext | Runs in the editor process |
ClientContext | Runs in game client |
ServerContext | Runs on dedicated server |
SmokeFilter | Fast; runs on every CI check-in |
ProductFilter | Project/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.
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.
// 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
// 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
// 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
// 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)
| Level | When to use |
|---|---|
Fatal | Crash-worthy unrecoverable errors |
Error | Operation failed, needs developer attention |
Warning | Unexpected but recoverable condition |
Display | User-visible output (always shown) |
Log | Standard development info |
Verbose | Detailed per-frame or per-call info |
VeryVerbose | Trace-level; very high frequency |
Structured Logging (UE 5.2+)
#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
// 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
// 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
// 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
| Situation | Macro |
|---|---|
| Class invariant, programmer error | check / checkf |
| Recoverable condition, want to continue | ensure / ensureMsgf |
| Expression has side effects always needed | verify / verifyf |
| Debug-build heavy validation | checkSlow |
| User-facing input validation | none — 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.
#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
// 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
// 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
// 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
// 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
#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:
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.
// 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 withUnrealEditor-Win64-DebugGamePDB in WinDbg or Rider. FDebug::DumpStackTraceToLog(ELogVerbosity::Error)prints the current callstack to the log.ensuresubmits 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 shipping — check() 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 basicsue-module-build-system— setting up a dedicated test module and target inclusionue-actor-component-architecture— AFunctionalTest placement and world interaction
References
references/automation-test-patterns.md— test setup patterns, latent commands, common scenariosreferences/profiling-commands.md— stat commands, Insights capture, analysis workflow