Skip to content

Instantly share code, notes, and snippets.

@tenpn
Created June 1, 2026 16:43
Show Gist options
  • Select an option

  • Save tenpn/c36c20254646cb559dbee1f9476b5075 to your computer and use it in GitHub Desktop.

Select an option

Save tenpn/c36c20254646cb559dbee1f9476b5075 to your computer and use it in GitHub Desktop.
The skeleton of an Unreal 5.5 Spec test, but using CQTest's more convenient Command Builder for latent operations
#include "Misc/AutomationTest.h"
#if WITH_AUTOMATION_TESTS
#include "CQTest.h"
#include "Commands/TestCommandBuilder.h" // FTestCommandBuilder - the fluent .Do()/.Until()/.Then() latent chain
#include "Containers/Ticker.h"
// ---------------------------------------------------------------------------------------------------------------------
// MINIMAL SKELETON: an Automation Spec (Describe/It, BDD) that drives latent steps with CQTest's FTestCommandBuilder.
//
// Why the bridge? Spec and CQTest each have their own latent machinery, and they don't natively combine:
// - A Spec enqueues ALL of its BeforeEach/It/AfterEach commands up front (when Define() builds the tree), so you
// cannot enqueue a latent command from inside an It() body - it would run AFTER AfterEach.
// - CQTest's FTestCommandBuilder.Build() instead hands you a single IAutomationLatentCommand you own and tick yourself.
//
// So we use a LatentIt (which gives us an FDoneDelegate to signal completion) and pump the built command on the core
// ticker, calling Done.Execute() once the queue drains. That's the whole trick - see RunCommands() below.
//
// ---------------------------------------------------------------------------------------------------------------------
BEGIN_DEFINE_SPEC(FCommandBuilderSkeletonSpec,
"AutomatedAF.RuntimeTests.Examples.CommandBuilderSkeleton",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter)
// Live tickers driving our built command queues, so AfterEach can tear any half-finished one down.
TArray<FTSTicker::FDelegateHandle> Tickers;
// Toy state the latent steps act on, to show .Do() (mutate) -> .Until() (wait) -> .Then() (assert) flowing across ticks.
int32 Counter = 0;
// The bridge: pump a built CQTest command on the core ticker; signal the Spec's Done when it finishes.
// (We can't enqueue it on the framework - a Spec has already queued all its commands by the time an It() runs.)
void RunCommands(const FDoneDelegate& Done, TSharedPtr<IAutomationLatentCommand> Commands)
{
if (!Commands.IsValid())
{
Done.Execute();
return;
}
Tickers.Add(FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda(
[Done, Commands](float) -> bool
{
if (Commands->Update()) // returns true once the whole .Do/.Until/.Then chain has drained
{
Done.Execute();
return false; // stop ticking this delegate
}
return true; // keep ticking next frame
})));
}
END_DEFINE_SPEC(FCommandBuilderSkeletonSpec)
void FCommandBuilderSkeletonSpec::Define()
{
// A Spec is one long-lived object and does NOT auto-reset between It()s - reset shared state here.
BeforeEach([this]() { Counter = 0; });
Describe("a latent command-builder chain", [this]()
{
LatentIt("runs Do -> Until -> Then across ticks", FTimespan::FromSeconds(30), [this](const FDoneDelegate& Done)
{
// Build the chain with the CQTest builder. Note: pass *this (the FAutomationTestBase) to the ctor so its
// asserts report against this test. Nothing runs until RunCommands() starts pumping Build().
FTestCommandBuilder Step(*this);
Step.Do(TEXT("kick something off"), [this]() { Counter = 1; })
.Until(TEXT("the work completes"), [this]() { return ++Counter >= 5; }, FTimespan::FromSeconds(10))
.Then(TEXT("assert the result"), [this]() { TestTrue(TEXT("counter reached target"), Counter >= 5); });
RunCommands(Done, Step.Build());
});
});
AfterEach([this]()
{
// Remove any tickers still alive (e.g. if a step failed mid-chain) so they can't bleed into the next It().
for (const FTSTicker::FDelegateHandle& H : Tickers) { FTSTicker::GetCoreTicker().RemoveTicker(H); }
Tickers.Reset();
});
}
#endif // WITH_AUTOMATION_TESTS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment