M5.1 — The Gauntlet plugin & TestController¶
What you'll learn
- What the optional Gauntlet plugin and its
TestControllerclass give you. - Where
TestControllerruns (inside the game) and what it's for (puppeteering + monitoring). - When a multi-step in-game sequence justifies reaching for it.
How it applies (QA)
Boot/smoke tests need no game-side code (M1.1). The moment a test must do things in the game in
sequence — load a level, spawn, act, observe — driving it purely from logs gets brittle.
TestController is the sanctioned place for that in-game scripting. Knowing when it's warranted
keeps you from either under-powering a test (log-scraping a complex flow) or over-engineering a
simple one.
Concepts¶
The optional plugin¶
Gauntlet ships an optional engine plugin that provides "a TestController class to assist with
puppeteering and monitoring a game instance." "Optional" is literal — you enable the plugin only
when a test needs in-game control. The docs note it's "particularly suited for smoke tests with
multiple execution steps" — i.e. exactly the multi-step in-game sequences that logs alone handle
poorly.
Where it runs and what it does¶
A TestController is C++ that runs inside the game process, not in the C# harness. Subclass it
for your test (conceptually UGauntletTestController), and it gets lifecycle hooks while the game
runs:
- an init hook when the controller starts,
- a tick hook called each frame/update,
- access to the live game — it can read game state and drive the game (open a level, possess a pawn, trigger an action) because it lives in the engine.
It both puppeteers (makes the game do things) and monitors (observes game state), then signals progress/outcome outward — typically by emitting log markers or setting state the C# test node reads (M5.2). It is the in-game agent of an out-of-game test.
Activating a controller¶
The controller is selected when the game launches — the C# test configures the role's command line
to enable the plugin and name the controller to run (commonly a -gauntlet=<ControllerName> style
switch). So the wiring is: the C# UnrealTestNode (M4) requests the role and tells that role's
process which TestController to instantiate on startup.
C# UnrealTestNode (harness) C++ TestController (in game)
│ configures role cmdline: │ OnInit(): set up the sequence
│ -gauntlet=MySmokeController ─────────▶ │ OnTick(): step the sequence,
│ │ drive + observe game
│ reads markers/state ◀── log markers ───────│ emit "StepN done" / final state
▼ ▼
decides verdict game does the actual in-world work
Verify on a real build: the base class name, the hook names, and the activation switch
(-gauntlet=…) vary by version. The model — an optional plugin, a C++ controller inside the game,
activated per-role from the C# side, communicating via markers/state — is the stable part.
When to use it (and when not)¶
- Use it when the test requires an ordered in-game sequence or game state the existing logs don't expose: "load TestArena, spawn, fire, confirm a hit registered, then quit."
- Skip it when existing logs already tell you what you need: boot tests, "reached menu," crash/ timeout checks. Adding a controller there is cost with no benefit (M1.1's boundary #2 still holds).
Pitfall: scripting the game where a log already answers
Writing a TestController to confirm something the game already logs adds C++ to maintain, a
plugin to keep enabled, and a second failure surface — for no new signal. Reach for the
controller when you need to cause in-game steps or read un-logged state, not to re-observe
what's already in the log.
Worked example — promote a smoke test to a controller¶
A team's smoke test currently: boot, and pass on the Reached MainMenu log. They now want: boot →
start a match → confirm the player can deal damage → quit cleanly.
- The new steps must happen in-game in order and depend on state (did damage apply?) that isn't in
the default logs. That crosses the threshold: it needs a
TestController. - Plan: a
MySmokeControllerwhose tick advances a small state machine —StartMatch→SpawnAndAct→ConfirmDamage→RequestQuit— emitting a marker at each step. The C# node passes the test when it sees theConfirmDamagemarker and a clean exit, fails on timeout or a missing step.
The boot half stayed log-only; only the new multi-step in-game part pulled in the controller.
Exercise 1 — Controller or not?
For each test, say whether it needs a TestController and why:
- Confirm the editor boots without errors.
- Drive the player through a tutorial's three scripted beats and confirm each fires.
- Confirm the shipping build reaches the main menu.
- Verify that picking up an item updates the player's inventory count in-game.
Exercise 2 — Place the responsibilities
For the promoted smoke test above, assign each to C# node or C++ controller: deciding the
overall pass/fail; opening TestArena; reading the ConfirmDamage marker; possessing the pawn
and firing.
Self-check — answers
Exercise 1: 1 no (boot/log only); 2 yes (ordered in-game beats, likely un-logged); 3 no (boot/log only); 4 yes (needs in-game state — inventory after pickup — that the default log won't expose).
Exercise 2: C# node — deciding overall pass/fail, reading the ConfirmDamage marker (it
consumes markers/logs from outside); C++ controller — opening TestArena, possessing the pawn
and firing (in-game actions). The split: harness decides and observes from outside; controller
acts inside.
Done when
- [ ] You can describe the Gauntlet plugin and
TestControlleras optional, in-game C++. - [ ] You can name what a controller does (puppeteer + monitor) and its lifecycle hooks.
- [ ] You can explain how a controller is activated from the C# side and how it communicates back.
- [ ] You can decide whether a given test warrants a controller or stays log-only.
Next: M5.2 — The C# / C++ split.