You can use Inversion of Control.
Unreal Engine actually has documentation of this (mentioned in the TTypeContainer write up) under a cpp file. It can be found here.
Reach out if you need any help.
Please note that the guide is out of date in terms of it not using describe
and it
. Here’s an example of how to do that:
And also please note that you may also need your own fixture class to add Actors etc into a virtual world and environment for the unit test:
WorldFixture.h
#pragma once
#include "CoreMinimal.h"
#include "EngineUtils.h"
/**
* https://minifloppy.it/posts/2024/automated-testing-specs-ue5/
*/
class LOYLY_API FWorldFixture
{
public:
explicit FWorldFixture(const FURL& URL = FURL())
{
if (GEngine != nullptr)
{
static uint32 WorldCounter = 0;
const FString WorldName = FString::Printf(TEXT("WorldFixture_%d"), WorldCounter++);
if (UWorld* World = UWorld::CreateWorld(EWorldType::Game, false, *WorldName, GetTransientPackage()))
{
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
WorldContext.SetCurrentWorld(World);
World->InitializeActorsForPlay(URL);
if (IsValid(World->GetWorldSettings()))
{
// Need to do this manually since world doesn't have a game mode
World->GetWorldSettings()->NotifyBeginPlay();
World->GetWorldSettings()->NotifyMatchStarted();
}
World->BeginPlay();
WeakWorld = MakeWeakObjectPtr(World);
}
}
}
UWorld* GetWorld() const { return WeakWorld.Get(); }
~FWorldFixture()
{
UWorld* World = WeakWorld.Get();
if (World != nullptr && GEngine != nullptr)
{
World->BeginTearingDown();
// Make sure to cleanup all actors immediately
// DestroyWorld doesn't do this and instead waits for GC to clear everything up
for (auto It = TActorIterator<AActor>(World); It; ++It)
{
It->Destroy();
}
GEngine->DestroyWorldContext(World);
World->DestroyWorld(false);
}
}
private:
TWeakObjectPtr<UWorld> WeakWorld;
};
Example usage:
#include "CoreMinimal.h"
#include "Actors/BeerActor.h"
#include "Characters/MainCharacter.h"
#include "Controllers/MainPlayerController.h"
#include "PlayerStates/MainPlayerState.h"
#include "Misc/AutomationTest.h"
#include "Tests/Fixtures/WorldFixture.h"
BEGIN_DEFINE_SPEC(BeerActorDrinkBeerSpec, "Loyly.Actors.BeerActor.DrinkBeerSpec", EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter)
TUniquePtr<FWorldFixture> WorldFixture;
ABeerActor* Actor;
AMainPlayerController* Controller;
AMainCharacter* Character;
AMainPlayerState* PlayerState;
END_DEFINE_SPEC(BeerActorDrinkBeerSpec)
void BeerActorDrinkBeerSpec::Define()
{
Describe("DrinkBeer", [this]()
{
BeforeEach([this]()
{
WorldFixture = MakeUnique<FWorldFixture>();
Actor = WorldFixture->GetWorld()->SpawnActor<ABeerActor>();
Controller = WorldFixture->GetWorld()->SpawnActor<AMainPlayerController>();
Character = WorldFixture->GetWorld()->SpawnActor<AMainCharacter>();
PlayerState = NewObject<AMainPlayerState>();
WorldFixture->GetWorld()->AddController(Controller);
Character->SetPlayerState(PlayerState);
Controller->Possess(Character);
});
It("should decrease the character temperature", [this]()
{
// Arrange
Actor->DecreaseTemperatureBy = 0.2;
PlayerState->Temperature = 1;
// Act
Actor->DrinkBeer();
// Assert
TestEqual("Temperature value decreased as expected", PlayerState->Temperature, 0.8f);
});
});
}
Also, you can test code outside of unreal engine being run which is handy if your computer sucks like mine.
Running via command line:
"C:\Program Files\Epic Games\UE_5.2\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" "C:\Users\james\Unreal Projects\Loyly\Loyly.uproject" -execcmds="Automation RunTests Loyly;Quit" -stdout -unattended -NOSPLASH -NullRHI
execcmds="Automation RunTests Loyly;Quit"
: This tells the Unreal Engine command-line editor to run automation tests that match the “Loyly” filter, and then quit the editor once the tests have been completed. It does not require an additional specification for EAutomationTestFlags::EditorContext because the test itself defines the context (and other flags) necessary for execution.stdout
: Forces the engine to output logs to the standard output, which can be very useful for CI systems to capture and analyze the output directly.unattended
: Runs the engine in a mode that does not require user interaction, suitable for automated tasks like continuous integration.NOSPLASH
: Disables the splash screen on startup, which is generally preferred for automated tasks to reduce overhead and potential graphical issues.NullRHI
: This is a crucial flag for running tests in environments without a dedicated GPU or where you do not want to initialize the full rendering hardware interface. It makes the tests run with a null renderer, which can prevent rendering-related operations but allows most other engine functionality to be tested.