UE Testing, Logging & Profiling
Target engine: UE 5.8. APIs below are verified against the 5.8 headers; older forms are listed under "Deprecated — do not use".
This skill owns logging, assertions, automation/functional testing, console commands and performance profiling for the repo. Logging, assertions, stats, trace and CVars live in Core (Logging/, Misc/AssertionMacros.h, Stats/Stats.h, ProfilingDebugging/, HAL/IConsoleManager.h). Debug drawing, the Visual Logger and AHUD::ShowDebug live in Engine. Test frameworks are the developer modules AutomationController, CQTest, FunctionalTesting and AutomationDriver; the Gameplay Debugger is the runtime module GameplayDebugger.
Context
Read .agents/ue-project-context.md if it exists (module names, conventions, enabled plugins, GAS/networking setup). Do not stop if it is missing. Identify the area from the request and the codebase; ask only when two plausible readings would produce different code.
Logging
Declaring categories
DefaultVerbosity must be <= CompileTimeVerbosity or the static_assert inside the category macro fires. Anything above CompileTimeVerbosity is deleted by the compiler, so DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, Log) removes every Verbose/VeryVerbose call site. Monolithic builds can cap all categories at once by defining COMPILED_IN_MINIMUM_VERBOSITY.
Verbosity levels
ELogVerbosity::All is an alias for VeryVerbose and is the usual CompileTimeVerbosity argument.
Emitting
Wrap an expensive report in if (UE_LOG_ACTIVE(LogMyGame, Verbose)) { … } so the work itself is skipped, not just the formatting.
Structured logging
UE_LOGFMT emits named fields that survive into Insights and log analysis; use {Name} placeholders, not %s.
Field names must match [A-Za-z0-9_]+ and be unique per event; values serialize through SerializeForLog or operator<<(FCbWriter&, T). UE_LOGFMT takes at most 16 fields — use UE_LOGFMT_EX with UE_LOGFMT_FIELD / UE_LOGFMT_VALUE beyond that.
Shipping and Test builds
NO_LOGGING is !USE_LOGGING_IN_SHIPPING in both Test and Shipping, and 0 in Debug and Development. Under NO_LOGGING, UE_LOG/UE_CLOG/UE_LOGFMT collapse to a Fatal-only path: every non-fatal call disappears, arguments included. Set bUseLoggingInShipping = true; in MyGame.Target.cs to define USE_LOGGING_IN_SHIPPING=1 and get logging back.
Controlling verbosity at runtime
From code use UE_SET_LOG_VERBOSITY(LogMyGame, Verbose); from config use [Core.Log] in DefaultEngine.ini.
Assertions
All in Misc/AssertionMacros.h. Choose by who is wrong: check for programmer error, ensure for a recoverable surprise worth reporting, a plain if for user or data input.
ensure/ensureMsgf report once per call site per session; ensureAlways/ensureAlwaysMsgf report every time. All four return the condition, so they compose with if.
USE_CHECKS_IN_SHIPPING defaults to 0 and is set by bUseChecksInShipping in the target rules, so Test builds also strip check and ensure by default. When DO_CHECK is 0, check(expr) degrades to CA_ASSUME(expr) and the expression is never evaluated; verify(expr) still evaluates it and only drops the halt.
Automation Tests
Test sources live in a Private/Tests folder of the module under test, or in a dedicated test module. Wrap each test file in #if WITH_DEV_AUTOMATION_TESTS (or WITH_AUTOMATION_TESTS = WITH_DEV_AUTOMATION_TESTS || WITH_PERF_AUTOMATION_TESTS) — the IMPLEMENT_* macros alone still compile the class, they only skip registration when WITH_AUTOMATION_WORKER is 0 (Misc/AutomationTest.h:4366); UBT turns both on for every configuration except Test and Shipping, overridable with bForceCompileDevelopmentAutomationTests, bForceCompilePerformanceAutomationTests and bForceDisableAutomationTests (UEBuildTarget.cs:6309-6325). Module and target wiring, the full pattern library and the command lines for running tests are in references/automation-test-patterns.md [blocked].
IMPLEMENT_COMPLEX_AUTOMATION_TEST adds virtual void GetTests(TArray<FString>& OutBeautifiedNames, TArray<FString>& OutTestCommands) const and runs RunTest once per entry. IMPLEMENT_CUSTOM_SIMPLE_AUTOMATION_TEST(TClass, TBaseClass, PrettyName, TFlags) declares a simple test whose base is a shared fixture you derive from FAutomationTestBase.
Flags
EAutomationTestFlags is an enum class with ENUM_CLASS_FLAGS, so combine with |. Two static_asserts enforce at least one context flag and exactly one filter flag.
EAutomationTestFlags_ApplicationContextMask covers every context flag in one token.
Assertions inside a test
The pattern is a regex unless you use the …Plain variants (AddExpectedMessagePlain, AddExpectedErrorPlain). AddExpectedError(Pattern, MatchType, Occurrences, bIsRegex) is the shorthand that matches warnings and errors (it forwards ELogVerbosity::Warning, which is inclusive); the AddExpectedMessage overload without a verbosity matches every severity.
Latent commands
RunTest returns before latent commands run. Enqueue them, then let Update() return true when the step is finished. Enqueuing after return true; is unreachable code.
DEFINE_LATENT_AUTOMATION_COMMAND through ..._FIVE_PARAMETER exist. To assert after the wait, derive from IAutomationLatentCommand and capture the FAutomationTestBase*.
Specs
BEGIN_DEFINE_SPEC(TClass, PrettyName, TFlags) / member declarations / END_DEFINE_SPEC(TClass) declares a BDD-style test whose body is void TClass::Define(). Inside Define() use Describe, It, LatentIt, BeforeEach, LatentBeforeEach, AfterEach (and xIt/xDescribe to disable a block). DEFINE_SPEC(TClass, PrettyName, TFlags) replaces the pair when the spec needs no members. Worked example in references/automation-test-patterns.md [blocked].
CQTest
CQTest (module CQTest) is the fixture-based front end over the same framework. Assert is a FNoDiscardAsserter, so every assertion goes through ASSERT_THAT, which returns from the method on failure.
TEST_CLASS(ClassName, TestDir) uses FNoDiscardAsserter and DefaultFlags (EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::ProductFilter). Variants: TEST_CLASS_WITH_FLAGS(ClassName, TestDir, Flags), TEST_CLASS_WITH_TAGS, TEST_CLASS_WITH_BASE, TEST_CLASS_WITH_ASSERTS. Lifecycle macros are BEFORE_ALL()/AFTER_ALL() (static, once per fixture) and BEFORE_EACH()/AFTER_EACH() (override Setup()/TearDown()). Asserter helpers: IsTrue, IsFalse, IsNull, IsNotNull, AreEqual, AreNotEqual, AreEqualIgnoreCase, AreNotEqualIgnoreCase, IsNear(Expected, Actual, Epsilon) — each also takes a trailing failure message.
World, actor and async helpers (TestCommandBuilder, FSpawnHelper, FActorTestSpawner, FMapTestSpawner, TObjectBuilder, UTestGameInstance) are covered in references/automation-test-patterns.md [blocked].
Functional Tests
AFunctionalTest (module FunctionalTesting) is an actor placed in a test map. PrepareTest, StartTest, IsReady_Implementation and OnTimeout are protected virtuals; FinishTest and the Assert* helpers are public.
Each override calls Super:: first. FinishTest(EFunctionalTestResult TestResult, const FString& Message) ends the run; EFunctionalTestResult is Default, Invalid, Error, Running, Failed, Succeeded, and Default lets the recorded assertions decide. Assertions are AssertTrue/AssertFalse (bool Condition, const FString& Message, const UObject* ContextObject = nullptr), AssertIsValid(UObject*, const FString&, const UObject* = nullptr), AssertEqual_Int/Float/Bool/Name/Object/Vector/Rotator/Transform/Quat and AssertValue_Int/Float/Double/DateTime (with an EComparisonMethod). Also TimeLimit, PreparationTimeLimit, SetTimeLimit(float, EFunctionalTestResult), LogMessage(const FString&) and StartStep/FinishStep. Full .cpp in references/automation-test-patterns.md [blocked].
Run every functional test in the current world with the Blueprint node UFunctionalTestingManager::RunAllFunctionalTests(WorldContextObject, bNewLog, bRunLooped, FailedTestsReproString) (the class is MinimalAPI and the function is not FUNCTIONALTESTING_API, so calling it from another module's C++ does not link), or from the command line:
Automation also accepts List, RunTest, RunFilter <Filter>, RunAll, SetFilter, SetPriority, SetMinimumPriority, Quit and SoftQuit; chain them with ;.
Profiling
Full command tables, the CSV and LLM workflows and the bottleneck-triage procedure are in references/profiling-commands.md [blocked].
Unreal Insights
Channel names drop the Channel suffix and lowercase: cpu, gpu, frame, bookmark, counters, log, loadtime, task, memalloc, callstack, module, metadata, net, rhicommands, rendercommands, slate, assetmetadata, iostore, animation, region, screenshot.
Instrumenting code
QUICK_SCOPE_CYCLE_COUNTER(STAT_MyAdHocScope) declares a stat inline for a one-off measurement; TRACE_INT_VALUE(TEXT("MyGame/Spawned"), Count) and TRACE_FLOAT_VALUE emit a counter with no declaration; SCOPED_NAMED_EVENT_TEXT("Name", FColor::Yellow) takes a literal and SCOPED_NAMED_EVENT_F formats one.
stat commands
stat unit first: it splits frame time into Game, Draw, RHIT and GPU. Then drill into the dominant thread with stat game, stat scenerendering, stat initviews, stat slate, stat threading, stat streaming, stat net, or stat MyGame for your own group. stat unitgraph plots the same numbers over time, stat fps shows frame rate, stat hitches flags spikes, stat namedevents emits stat names as profiler events for external tools.
Memory
Launch with -LLM and -trace=memalloc,callstack,module for Memory Insights; scope allocations with LLM_SCOPE(ELLMTag::EngineMisc) or a custom tag declared via LLM_DEFINE_TAG/LLM_DECLARE_TAG and entered with LLM_SCOPE_BYTAG. On Windows, non-editor builds default to MallocBinned3; override with -binnedmalloc2, -binnedmalloc, -ansimalloc or -stompmalloc.
Hitch capture and UObject count
snapshothitches -start arms an automatic trace snapshot whenever the stats system detects a hitch; snapshothitches -stop disarms it. It needs STATS and a non-Shipping build, and disables the Screenshot channel while armed so a stale screenshot cannot land in the snapshot tail.
Compiling with CSV_TRACK_UOBJECT_COUNT=1 (with CSV profiling enabled) records the live UObject count every frame as CSV stat Total in category ObjectCount, read from UObjectStats::GetUObjectCount(). Use it to catch object leaks over a long session without a full memory capture.
Debug Visualization
Debug drawing
Everything in DrawDebugHelpers.h sits behind ENABLE_DRAW_DEBUG, which is UE_ENABLE_DEBUG_DRAWING = (!(UE_BUILD_SHIPPING || UE_BUILD_TEST) || WITH_EDITOR). In Test or Shipping game builds they become empty inline stubs (DrawDebugHelpers.h:180-185; defining SHIPPING_DRAW_DEBUG_ERROR=1 removes them so stray calls fail to compile), but arguments are still evaluated — wrap expensive call sites in #if ENABLE_DRAW_DEBUG.
The Blueprint-facing UKismetSystemLibrary::DrawDebugLine/Arrow/Sphere/Capsule/String gate themselves, take FLinearColor and an EDrawDebugSceneDepthPriorityGroup, and are joined by PrintString(WorldContextObject, InString, bPrintToScreen, bPrintToLog, TextColor, Duration, Key).
Visual Logger
ENABLE_VISUAL_LOG is PLATFORM_DESKTOP && !NO_LOGGING && UE_ENABLE_DEBUG_DRAWING. Open the timeline with Window > Visual Logger.
Every macro early-outs on FVisualLogger::IsRecording(), so the arguments cost nothing when recording is off.
Gameplay Debugger and ShowDebug
Press ' in PIE. Register a category from your module's StartupModule (add "GameplayDebugger" to PrivateDependencyModuleNames; wrap the registration and the category class in #if WITH_GAMEPLAY_DEBUGGER, which is 0 in Test/Shipping game builds by default — GameplayDebuggerCategory.h:10-11, TargetRules.cs:1125):
FMyDebuggerCategory derives from FGameplayDebuggerCategory and overrides virtual void CollectData(APlayerController* OwnerPC, AActor* DebugActor) and virtual void DrawData(APlayerController* OwnerPC, FGameplayDebuggerCanvasContext& CanvasContext), feeding them with AddTextLine and AddShape. Call UnregisterCategory(TEXT("MyGame")) in ShutdownModule.
AHUD::ShowDebug(FName DebugType) toggles a named debug page (ShowDebug AI, ShowDebug Physics); ShowDebugToggleSubCategory(FName) toggles a sub-page and ShowDebugForReticleTargetToggle(TSubclassOf<AActor>) retargets it at whatever the reticle is over. Override virtual void ShowDebugInfo(float& YL, float& YPos) or bind AHUD::OnShowDebugInfo for your own page. The separate display, displayall and displayclear console commands print a named property of an object or class onto the HUD.
Console Commands and CVars
Read a CVar with GetValueOnGameThread(), GetValueOnRenderThread() or GetValueOnAnyThread(). Common flags: ECVF_Default, ECVF_Cheat (disabled on shipping targets), ECVF_ReadOnly, ECVF_Scalability, ECVF_RenderThreadSafe. When a command's lifetime is tied to an object, register at runtime with IConsoleManager::Get().RegisterConsoleCommand(Name, Help, Delegate) and release the returned IConsoleCommand* with IConsoleManager::Get().UnregisterConsoleObject(Cmd).
Deprecated — do not use
Common Mistakes
Assuming Test builds keep check/ensure: DO_CHECK and DO_ENSURE are USE_CHECKS_IN_SHIPPING / USE_ENSURES_IN_SHIPPING in both Test and Shipping, and both default to 0. Use verify() when the expression must always run, or set bUseChecksInShipping.
Logic inside a log or assert: UE_LOG(LogMyGame, Log, TEXT("%d"), AdvanceCounter()); stops advancing once NO_LOGGING is on, and check(Init()) stops initializing once DO_CHECK is 0. Compute into a local first, then log or assert on it.
Using Log and expecting console output: Display prints to the console; Log only reaches the log file. "My log line never appears" is almost always a Log-verbosity message.
Missing filter flag: every IMPLEMENT_*_AUTOMATION_TEST, DEFINE_SPEC and BEGIN_DEFINE_SPEC needs exactly one of SmokeFilter/EngineFilter/ProductFilter/PerfFilter/StressFilter/NegativeFilter plus at least one context flag, or the static_assert fails to compile.
Dropping a CQTest assertion result: the asserter methods are [[nodiscard]], so Assert.IsTrue(x); warns and keeps running after a failure. Always write ASSERT_THAT(IsTrue(x));.
DrawDebug* in a Shipping or Test game build: the calls become no-op stubs but their arguments (traces, string formatting) still run. Wrap the whole block in #if ENABLE_DRAW_DEBUG.
Skipping Super::StartTest(): AFunctionalTest::StartTest fires the Blueprint ReceiveStartTest event, so omitting Super:: silently disables every Blueprint-authored step in the same test actor.
Related Skills
ue-cpp-foundations— UObject basics, delegates, containers, the UFUNCTION/UPROPERTY specifier tablesue-module-build-system— Build.cs and Target.cs wiring,bBuildDeveloperTools, test module setup, plugin layoutue-editor-tools— editor utilities, commandlets and Slate tooling that host test and debug UIue-physics-collision— trace and sweep queries whose results you visualize withDrawDebug*ue-ai-navigation— behaviour trees, EQS and the AI-side Gameplay Debugger categoriesue-async-threading— task graph and async work that latent commands and thetasktrace channel observeue-gameplay-framework— GameMode, PlayerController, HUD and CheatManager, the hosts forExecfunctionsue-materials-rendering— material instances, parameter collections, render targets and post process


