Skip to content

M5.1 — The Gauntlet plugin & TestController

What you'll learn

  • What the optional Gauntlet plugin and its TestController class give you.
  • Where TestController runs (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 MySmokeController whose tick advances a small state machine — StartMatchSpawnAndActConfirmDamageRequestQuit — emitting a marker at each step. The C# node passes the test when it sees the ConfirmDamage marker 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:

  1. Confirm the editor boots without errors.
  2. Drive the player through a tutorial's three scripted beats and confirm each fires.
  3. Confirm the shipping build reaches the main menu.
  4. 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 TestController as 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.