Automation & testing
Unreal has a layered test ecosystem. Each layer trades world setup cost against richness:
| Layer | Runs | Best for |
|---|---|---|
| Low-level tests (LLT) | thin process, no editor/game | pure C++ module logic, fastest CI |
| Simple automation test | editor or commandlet | unit/feature tests, no world required |
| Complex automation test | editor or commandlet | same code over many inputs (content stress) |
Spec (DEFINE_SPEC) | editor or commandlet | BDD-style behavior docs, latent/async scenarios |
Functional test (AFunctionalTest) | full world in a test level | gameplay integration, AI, abilities |
| Gauntlet | full device sessions | large-scale, perf, device/platform testing |
When to use this skill
- Writing C++ unit or integration tests for game systems (damage, inventory, save data).
- Expressing multi-case behavior specs that should read like documentation.
- Testing gameplay mechanics that require actors, physics, or a running world.
- Running tests headless in CI on every commit.
- Reaching devices or larger sessions via Gauntlet.
Mental model
Tests live outside UObject reflection — they are not Blueprints-visible. The
FAutomationTestBase base class provides the assertion API (TestTrue, TestEqual, …)
and is instantiated once as a global at startup. RunTest must return true; use the
assertion methods rather than check/ensure so failures record cleanly instead of
crashing.
EAutomationTestFlags controls two orthogonal dimensions: context (where the test
can run: EditorContext, ClientContext, ServerContext, CommandletContext,
ProgramContext) and filter (which CI bucket: SmokeFilter, EngineFilter,
ProductFilter, PerfFilter, StressFilter). Every test must specify at least one
context and one filter.
Simple automation test
cpp// Private/Tests/DamageTest.cpp #include "Misc/AutomationTest.h" IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDamageMathTest, "MyGame.Combat.DamageMath", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FDamageMathTest::RunTest(const FString& Parameters) { const float Result = ComputeDamage(100.f, 0.5f); TestEqual(TEXT("Half damage"), Result, 50.f); TestTrue(TEXT("Non-negative"), Result >= 0.f); return true; // returning false also marks the test failed }
Key rules:
- Place tests in
Private/Tests/of the relevant module; name the file<ClassFilename>Test.cpp. - The dotted path string (
"MyGame.Combat.DamageMath") is what the browser and command-line filter use. - Always combine a context flag and a filter flag with
|. - Add
"AutomationController"and"AutomationUtils"toPrivateDependencyModuleNamesin yourBuild.csif you need automation utilities.
Complex automation test
Complex tests run the same RunTest body over a set of inputs provided by
GetTests. Use them for content stress (load all maps, compile all Blueprints):
cppIMPLEMENT_COMPLEX_AUTOMATION_TEST(FLoadAllMapsTest, "MyGame.Maps.LoadAll", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) void FLoadAllMapsTest::GetTests(TArray<FString>& OutBeautifiedNames, TArray<FString>& OutTestCommands) const { // populate OutBeautifiedNames and OutTestCommands in parallel } bool FLoadAllMapsTest::RunTest(const FString& Parameters) { // Parameters is one entry from OutTestCommands return true; }
Spec style (BDD)
Specs express behavior in Describe/It blocks with scoped BeforeEach/AfterEach
setup. Each It becomes an independently runnable test. Use BEGIN_DEFINE_SPEC to
add member variables; DEFINE_SPEC when the class body can stay empty.
cppBEGIN_DEFINE_SPEC(FInventorySpec, "MyGame.Inventory", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) TSharedPtr<FInventorySystem> Inv; END_DEFINE_SPEC(FInventorySpec) void FInventorySpec::Define() { BeforeEach([this]() { Inv = MakeShared<FInventorySystem>(); }); Describe("AddItem", [this]() { It("should increase count by one", [this]() { Inv->AddItem(TEXT("Sword"), 1); TestEqual(TEXT("Count"), Inv->GetCount(TEXT("Sword")), 1); }); It("should reject a stack over the limit", [this]() { TestFalse(TEXT("Over limit"), Inv->AddItem(TEXT("Arrows"), 9999)); }); }); AfterEach([this]() { Inv.Reset(); }); }
- Name spec files
<FeatureName>.spec.cpp— not*Test.cpp. xDescribe/xItdisable a block without deleting it.- Use
LatentIt/LatentBeforeEachwhen a test step spans multiple frames (passes aFDoneDelegate). - Pass
EAsyncExecution::TaskGraph/::ThreadPooltoIt/BeforeEachto control which thread each block runs on.
See references/spec-and-latent-commands.md
for BeforeEach scoping, parameterized tests, async/latent patterns, and Redefine.
Functional tests (in a level)
AFunctionalTest is a UCLASS actor placed in a dedicated test map. The automation
runner discovers it through the Functional Testing Editor plugin, starts each
actor, and collects results.
Override the key virtual functions in a C++ child class:
cpp// MyFunctionalTest.h UCLASS() class MYGAME_API AMyFunctionalTest : public AFunctionalTest { GENERATED_BODY() protected: virtual void PrepareTest() override; // start async setup (streaming, server) virtual bool IsReady_Implementation(); // return false while still loading virtual void StartTest(); // main test body };
cpp// MyFunctionalTest.cpp — inside StartTest: void AMyFunctionalTest::StartTest() { AMyActor* Target = Cast<AMyActor>(FindActorByTag(TEXT("Target"))); AssertIsValid(Target, TEXT("Target actor found")); AssertTrue(Target->Health > 0.f, TEXT("Target is alive")); FinishTest(EFunctionalTestResult::Succeeded, TEXT("OK")); }
Important:
FinishTestmust be called or the test times out. SetTimeLimitin the actor's details to bound CI runs.- Use
AssertTrue/AssertIsValid/AssertEqual_Float(the functional-test assert family fromAFunctionalTest) rather than theFAutomationTestBasemacros. - Register spawned actors with
AddActorToDestroyOnTestEndso cleanup is automatic.
See references/functional-tests.md for PrepareTest
and IsReady patterns, Ground Truth Data, and Blueprint functional test setup.
Low-level tests (Catch2 / LLT)
LLTs compile a separate thin executable per module using Catch2 — no editor, no
game framework — giving the fastest possible CI iteration for module-internal logic.
They live in a Tests/ directory alongside the module and require a dedicated
<Module>Tests.Target.cs build target. See the official doc and
Developer/LowLevelTestsRunner/ for the runner harness.
cpp#include "TestHarness.h" // Developer/LowLevelTestsRunner/Public/TestHarness.h TEST_CASE("MyModule / FormatFString / basic substitution", "[smoke]") { FString Out = FString::Printf(TEXT("v=%d"), 42); CHECK(Out == TEXT("v=42")); }
LLTs are the right choice when your logic has no UObject or world dependency and you want sub-second CI feedback. See references/low-level-tests.md for target setup and build invocation.
Assertion reference
FAutomationTestBase assertions (all record failures without stopping execution unless
you return false or call UE_RETURN_ON_ERROR):
| Assertion | Notes |
|---|---|
TestTrue(What, Expr) | fails if Expr is false |
TestFalse(What, Expr) | fails if Expr is true |
TestEqual(What, Actual, Expected) | float/double overloads accept a Tolerance |
TestNotNull(What, Ptr) | fails if pointer is null |
TestNull(What, Ptr) | fails if pointer is non-null |
TestValid(What, Value) | TWeakObjectPtr / TOptional validity |
AddError(Message) | unconditional failure record |
AddWarning(Message) | non-failing annotation |
UE_RETURN_ON_ERROR(Cond, Msg) | record error and return false if condition fails |
Running tests
Editor: Tools → Test Automation → Automation tab → select tests → Start Tests.
Command line / CI (headless):
UnrealEditor-Cmd.exe MyProject.uproject -ExecCmds="Automation RunTest MyGame.Combat;Quit" -unattended -nopause -nullrhi -ReportExportPath="TestResults/"
Flags:
-ExecCmds="Automation RunTest <filter>"— dotted prefix runs all tests under it.-ExecCmds="Automation RunTest Group:MyGroup;Quit"— named test group (configured viaAutomationTestGroupinDefaultEngine.ini).-ReportExportPath— writes JSON + HTML for a report server.-ResumeRunTest— resume an interrupted run from a saved JSON report.
Parse the exit code and log for pass/fail in your CI pipeline. A non-zero exit code
indicates test failures when -unattended is set.
Gotchas
- Missing both a context and a filter flag → the test never appears in the browser.
- Using
check/ensureinstead ofTestTrue/AddError→ crashes the whole run instead of recording a failure. - Functional test with no timeout → CI hangs indefinitely.
- Non-deterministic tests (frame timing, RNG, network) → flaky; seed or mock inputs.
- Spec variable capture inside
Describe— local variables captured by lambda go out of scope before theItruns; capturethisand use member variables. - Test module depends on the editor — tests that need
WITH_EDITORtypes must be placed in anEditor-category module; don't put them inRuntime. AddErrorS/AddWarningS— deprecated since 5.4; useAddError/AddWarning.TestEqualInsensitive— deprecated since 5.5; useTestEqual(strings are case-insensitive by default now).
Version notes
EAutomationTestFlags_ApplicationContextMaskis a convenience constant combining all context flags; commonly used in specs:EAutomationTestFlags::ProductFilter | EAutomationTestFlags_ApplicationContextMask.- Priority flags (
CriticalPriority,HighPriority, etc.) were added in UE5; they are optional but useful for CI triage. - Low-level tests (Catch2-based LLTs) are the recommended direction for new pure-logic
tests added since UE 5.0. The
FAutomationTestBasestack remains the right choice for tests that need the engine or editor context.
References & source material
Engine source (UE 5.8):
Runtime/Core/Public/Misc/AutomationTest.h—EAutomationTestFlags:88,FAutomationTestBase:1594,RunTest:2653,TestEqual:1985,TestFalse:2367,TestNull:2530,TestTrue:2603,FAutomationSpecBase:2899,Describe:2757,BeforeEach:2821,It:2789,IAutomationLatentCommand:525,DEFINE_LATENT_AUTOMATION_COMMAND:3796,ADD_LATENT_AUTOMATION_COMMAND:4087,IMPLEMENT_SIMPLE_AUTOMATION_TEST:4297,DEFINE_SPEC:4339,BEGIN_DEFINE_SPEC:4346.Developer/FunctionalTesting/Classes/FunctionalTest.h—AFunctionalTest:249,FinishTest:678,PrepareTest:777,IsReady_Implementation:814,AddError:659.Developer/LowLevelTestsRunner/Public/TestHarness.h— Catch2 bridge/harness header.Developer/CQTest/—CQTestmodule (TEST,TEST_CLASS,ASSERT_THATmacros).
Official docs (UE 5.8):
- Automation Test Framework — https://dev.epicgames.com/documentation/unreal-engine/automation-test-framework-in-unreal-engine
- Write C++ Tests — https://dev.epicgames.com/documentation/unreal-engine/write-cplusplus-tests-in-unreal-engine
- Automation Spec — https://dev.epicgames.com/documentation/unreal-engine/automation-spec-in-unreal-engine
- Functional Testing — https://dev.epicgames.com/documentation/unreal-engine/functional-testing-in-unreal-engine
- Run Automation Tests — https://dev.epicgames.com/documentation/unreal-engine/run-automation-tests-in-unreal-engine
- Low-Level Tests — https://dev.epicgames.com/documentation/unreal-engine/low-level-tests-in-unreal-engine
- Gauntlet Automation Framework — https://dev.epicgames.com/documentation/unreal-engine/gauntlet-automation-framework-in-unreal-engine
- CQTest — https://dev.epicgames.com/documentation/unreal-engine/cqtest-test-framework-for-unreal-engine
Deep-dive references in this skill:
- references/spec-and-latent-commands.md —
Spec scoping, parameterized tests, latent/async patterns,
Redefine. - references/functional-tests.md —
AFunctionalTestlifecycle, Ground Truth Data, Blueprint setup. - references/low-level-tests.md — Catch2/LLT target setup, build invocation, CQTest.
- references/flags-and-ci.md —
EAutomationTestFlagsreference table, CI command patterns, report server, Gauntlet overview.
Related skills: ue-logging-and-assertions, ue-debugging-techniques.


