# Unreal Engine: Testing and Profiling

*2025-10-29*

> Notes taken from the eighteenth week of Game Programming Foundations


After last week's [state machines]({{< relref "/posts/2025/unreal-engine-17-state-machines" >}}), this week was about **testing and profiling**: the [Automation Test Framework]https://dev.epicgames.com/documentation/unreal-engine/automation-test-framework-in-unreal-engine, [functional and unit testing](https://dev.epicgames.com/documentation/unreal-engine/low-level-tests-in-unreal-engine), and [performance analysis](https://dev.epicgames.com/documentation/unreal-engine/introduction-to-performance-profiling-and-configuration-in-unreal-engine) with Unreal Insights – the discipline that keeps everything built so far from quietly rotting.

Notes below.

---

## Automated Testing

The starting questions: what can go wrong, and what do you test to build your test cases?

The motivating example from the course: a third-person stealth game whose **core feature is AI perception**. You want to guarantee AI sight always works. But with a large world and many levels containing spawners, testing a change by playing through the entire game is tedious – so you set up tests to make sure the major feature isn't broken.

### Error handling vs testing

An important distinction – especially between **silent error handling** and **asserts/breaks**:

- Asserts cause game-breaking crashes: execution does not progress beyond the assert. They're needed to ensure required pointers or objects are valid – preventing a whole class of bugs caused by a missing pointer or object
- But asserts (and `Ensure`) only catch what they're told to catch. They won't catch **logic errors** – only testing catches those
  - Example: an enemy killing a friendly because the logic never checked whether the target is an ally. No pointer is null; nothing asserts. A test map with two allies, run for a number of seconds, asserting that neither ally took damage, *catches it*

## Functional Testing

The AI perception demo as a functional test:

- Create a folder for test maps and assets
- Create a map for the test
- Create a Blueprint of type **Functional Test**
- Fill in the logic and call the **Finish Test** node - without it, the test times out and reports failure
- Create variables and functions to keep the test's logic legible

**Keeping tests alive:** functional tests must not go stale. When code or logic changes, the test gets updated or deleted. Stale tests become like overwhelming warning messages – they get ignored, and eventually the whole test suite gets ignored because nobody can tell whether a test failed or simply stopped being relevant.

## Unit Testing

Examples of what unit tests are good for:

- Leaderboards: unit testing that the scoring logic ranks correctly
- Stress-testing a spatial algorithm: generate random coordinates, find the closest point, and brute-force the expected answer to verify the sort
- A quadtree implementation: feed it thousands of locations to verify the getters return what's intended
- Small utilities: string lengths, floating-point precision

## Profiling

Performance is a key factor in build stability: as the game grows in complexity, performance drops. **Unreal Insights** tracks framerate and shows *why*.

Context for the numbers: a frame processes game logic, rendering, and various engine/game code. If the logic takes too long to process, the game slows down. The ideal framerate is 60; some games ship at 30; VR targets 90+.

**Workflow with the test project** (a Scan feature on the player doing a sphere trace, tied to an input action):

1. Start a **trace** - the button in the lower right starts it; press again to stop
2. Parse the information looking for spikes - There's *always* a spike when the game loads in: BeginPlay and Construction Script events. Expected, not a bottleneck
3. Click on a frame to see the breakdown of functions called and their timing
4. Filter for **Sphere Cast** and double-click the function to see the time taken
5. Remove other charts to reduce noise; zoom out to see the sphere cast in isolation
6. Click anywhere on the timeline to show that frame's breakdown; clicking **PerformScan** shows the detailed breakdown of functions called

**What this bought us:** a direct pointer at the bottleneck. Vector math calls turn out to be very fast – the slow part was elsewhere.

Quick console alternative: `stat Unit` brings up the per-frame timing stats overlay.

---

## Notes on things that stood out

Two things this week reframed how I think about the whole course's worth of work. First, the assert/testing distinction is really a humility lesson: asserts catch the bugs you *imagined*, tests catch the bugs you didn't. The friendly-fire example is perfect – nothing crashes, nothing logs, the game is just wrong.

Second, the stale-test warning is sneakier than it sounds. A test suite with a few dead tests trains you to ignore red, which is functionally identical to having no test suite – with all the maintenance cost and none of the benefit. Tests are code, and code rots.

And `stat Unit` is going to be muscle memory from here on out.
