UE4 Automation Testing
9 minute read.
Have you ever had a feature in an Unreal Engine game that you had to come back to again and again, because it was a nest of bugs? This gives you this uneasy feeling of that part being unstable, not really done yet, you can never relax… right?
What if you could guarantee that part of your code works by testing it in a few seconds, rather than minutes of manual testing?
Automated testing or “Automation Tests” are the solution to your problem!
Automation Tests
In non-game applications testing is a vital part of software projects. Automation tests are small programms that test your code for correctness. The tests can be used while developing a feature and ensure it actually does what it is supposed to, or to ensure something still works after you made changes to that code later down the line (so called “regression testing” 1). In game development it is less popular, but equally useful.
A test works by e.g. calling a submit score function and then getting all entries of the highscore list and making sure the new entry appeared. Basically what you would probably do manually otherwise.
The “Automation” in “Automation Testing” means that you merely have to click a button and all your little test applications run without futher manual labor involved. This makes this super fast and easy to do, which makes you do it more often.
Framework in UE4
Fortunately Unreal Engine has a framework for you that makes writing automation tests easier. It especially helps you with asynchroneous calls, e.g. when telling the game to load a map and then needing to wait for it to be loaded before you can start testing with Actors inside that map.
Unfortunately their documentation on it is pretty out of date. Instead I had to browse the Automation framework’s source code and their engine tests to figure out how to properly use it.
Since I want to save you the time and pain, here is an up-to-date guide for you:
Test Plugin Setup
It is recommended to keep your tests separated from the rest of your game
(or plugin) as at least a module or even as a plugin.
This is beneficial, since you can exclude the test code from the final game
build due to the module’s "Type": "Developer"
configuration. Having it in a
separate plugin would allow you to even disable it manually, though I cannot
come up with a real use case for that.
So go ahead and create a new folder in your projects Plugin/
directory.
We’ll call your game —I feel outrageously creative today—YourGame
for
the sake of this tutorial and therefore the plugin will be called YourGameTests
.
The YourGameTests.uplugin
looks like this:
{ "FileVersion": 3, "Version": 1, "VersionName": "1.0.0", "FriendlyName": "Your Game Tests", "Description": "Automation tests for YourGame.", "Category": "Testing", "CreatedBy": "You", "CanContainContent": true, "Installed": false, "Modules": [ { "Name": "YourGameTests", "Type": "Developer", "LoadingPhase": "Default" } ] }
With that we’re ready to set up a Source/YourGameTests
folder in there and
add our module’s build file. YourGameTests.Build.cs
should contain:
using UnrealBuildTool; public class YourGameTests : ModuleRules { public YourGameTests(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PrivateDependencyModuleNames.AddRange(new string[] { "Core", "Engine", "CoreUObject", "YourGame" }); } }
You add YourGame
to the dependencies to make its classes and functions available to
the test.
Next, we add the module scaffold in YourGameTests.cpp
and YourGameTests.h
. While
latter can be empty, the former contains the following three lines:
#include "YourGameTests.h" #include "Modules/ModuleManager.h" IMPLEMENT_MODULE(FDefaultModuleImpl, YourGameTests);
Time to add our first little test!
Writing a Test
Out of habbit, I put all my test sources into a Tests
subfolder. This would allow
me to merge it with the game module without mixing test and game sources, should I ever
want to do that—probably not—but other than that, you can just aswell put your sources
directly next to the .Build.cs
.
Add a source file for the tests with your name. E.g. MenuTest.cpp
or MatchmakingTest.cpp
or HighscoresTest.cpp
etc. I will go with GameTest.cpp
which is supposed to test general
functionality of the game. It will look like this:
#include "YourGameTests.h" #include "Misc/AutomationTest.h" #include "Tests/AutomationCommon.h" #include "Engine.h" #include "EngineUtils.h" #include "YourGameModeBase.h" #include "MyEssentialActor.h" // Copy of the hidden method GetAnyGameWorld() in AutomationCommon.cpp. // Marked as temporary there, hence, this one is temporary, too. UWorld* GetTestWorld() { const TIndirectArray<FWorldContext>& WorldContexts = GEngine->GetWorldContexts(); for (const FWorldContext& Context : WorldContexts) { if (((Context.WorldType == EWorldType::PIE) || (Context.WorldType == EWorldType::Game)) && (Context.World() != nullptr)) { return Context.World(); } } return nullptr; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FGameTest, "YourGame.Game", EAutomationTestFlags::Editor | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ProductFilter) bool FGameTest::RunTest(const FString& Parameters) { AutomationOpenMap(TEXT("/Game/Levels/StartupLevel")); UWorld* world = GetTestWorld(); TestTrue("GameMode class is set correctly", world->GetAuthGameMode()->IsA<YourGameModeBase>()); TestTrue("Essential actor is spawned", TActorIterator<AMyEssentialActor>(world)); return true; }
The GetTestWorld()
function is essential to get some handle to the world. It’s hidden in AutomationCommon.cpp
,
so I copied it out.
Alternatively to TestTrue
, there are TestNotNull
, TestEqual
and so on as you would expect in most testing framworks
(and most of which are superfluous).
Latent Commands
Sometimes you will need to allow your game to tick a couple of times before testing a result. Assume for example, you need wait for a ball falling down on a pressure pad. You would check every tick whether the ball has reached a certain Z location yet, and only if it did check whether the pressure pad has been triggered or something.
(In this particular case it’s probably a better idea to just set the position of the ball immediately which also speeds up the test.)
Similarly for ansynchroneous loading levels: you will have to wait until UE4 loaded your level to then start testing on it.
The Automation Testing Framework provides “Latent Commands” to do this. They
have an bool Update() method, which you override to return false
to wait and true
when you’re done. This means you can wait for some condition to hold true until you do your
testing.
FMyCommand
could look like this:
DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FMyCommand, FGameTest*, Test); bool FMyCommand::Update() { if(!SomethingHasHappened) return false; // Try again later Test->TestTrue("SomethingWasCaused", SomethingWasCaused); return true; // Command completed }
And to enqueue the latent command, you use
ADD_LATENT_AUTOMATION_COMMAND(FMyCommand(this));
Note, that all test code after that needs to also be added as latent automation command, because otherwise it is not guaranteed that this command is completed before your code runs. This makes working with these very tedious.
this
is a parameter which allows calling Test*()
functions from the automation command. You can
pass anything else, also.
Be aware that the macros for more params are missing the “S” in “PARAMETERS”,
it’s just DEFINE_LATENT_ATUOMATION_COMMAND_TWO_PARAMETER
(i.e. doesn’t follow the DELCARE_DYNAMIC_DELEGATE_*Params
convention).
Advanced Testing
For this blog post I decided to publish our “AdvancedTesting” plugin onto the Unreal Engine Marketplace. It allows you to specify your latent automation commands “inline”, hence looking something like this:
#include "AdvancedTesting.h" // ... bool FMyTest::RunTest(const FString& Parameters) { AddInlineLatentCommand([]() { return HasEventOccured; }); AYourGameMode* myGameMode; AddInlineLatentCommand([](AYourGameMode* gm) { return gm->HasEventOccured(); }, myGameMode); }
It also has a couple of other neat utilities.
If this article was helpful to you and want to support my work, it is available here. 2 Otherwise, you can also grab it from the GitHub repository, once it’s released under an MIT license, for free! (MIT license allows you to use it commercially, yes ;) )
- 1
- A “regression” is a bug in code that worked at some point in time, whereas a normal bug is something that never worked ever.
- 2
- Currently pending approval.
Written in 90 minutes, edited in 15 minutes.