Best Practices

These practices are inspired by Playwright's design philosophy and adapted for Unreal Engine game development. Follow these principles to keep your test suite fast, scalable, and resilient to engine changes.

1Use Auto-Retrying Assertions (No Sleeps)

Games are inherently asynchronous. Animations take time to finish blending, latent physics forces take time to resolve, and RPCs take time to arrive from the server.

Anti-Pattern: Single Snapshot Reading
// ❌ BAD: No retry, single snapshot
const text = await modal.getChild('TitleText').getText();
expect(text).toBe('Are you sure?');
  • No Retries: Fetches the value once at the exact millisecond. Flakily fails if UMG animations or network packets take 50ms to process.
  • No Reporting Context: HTML reports just show expect(received).toBe(expected) with no idea which widget or property failed.
Recommended: Auto-Retrying Matchers
// ✅ GOOD: Auto-polls engine ticks up to 5s
await expect(modal.getChild('TitleText'))
  .toHaveText('Are you sure?');
  • Auto-Retrying: Listens for state updates and resolves the moment the condition is met without manual delays.
  • Rich Matchers: Supports toHaveText, toContainText, toHaveValue, toBeVisible, and toBeEnabled.
  • HTML Report Steps: Clear breadcrumbs in trace reports.

2Use the Game Object Model (GOM)

Web developers use the Page Object Model (POM); game developers use the Game Object Model. Do not leak raw Blueprint string paths or widget hierarchy names directly into your test files. Encapsulate them inside specialized TypeScript classes.

If a Blueprint path changes or an ability tag is renamed, you only have to update it in one place. See our Game Object Model Guide.

3Use Fixtures for Test Isolation

Tests should never depend on each other. When a test fails, it should be able to run individually and produce the exact same failure.

Use Playwright's test.extend() to create isolated environments for each test. Specter handles connecting to the engine, spinning up the map, spawning actors, and cleaning them up before the next test runs.

4Test the Game, Not the Implementation

Tests should verify player-visible outcomes, not internal variables. If you test internal implementation details, your tests will break every time a designer refactors a Blueprint.

  • Bad: Checking if bIsStunned == true on the Character Movement Component.
  • Good: Checking if the character possesses the Status.Stunned Gameplay Tag, and asserting that their location hasn't changed.

5Locators, Not Coordinates

Never interact with a UI widget by clicking an absolute screen coordinate (X/Y). Screen resolutions, aspect ratios, and safe zones will immediately break these tests.

Instead, use world.widget('WidgetName') to locate the specific UI element in the UMG hierarchy, and let Specter calculate the dynamic bounding box at runtime.