Best Practices

These practices are inspired by Playwright's legendary design philosophy but have been uniquely adapted for the realities of Game Development. By following these core principles, you'll ensure that your Unreal Engine test suite remains fast, scalable, and most importantly, 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: Playwright HTML reports just show expect(received).toBe(expected) with no idea which widget or property caused the failure.
Recommended: Auto-Retrying Matchers
// ✅ GOOD: Auto-polls engine ticks up to 5s
await expect(modal.getChild('TitleText'))
  .toHaveText('Are you sure?');
  • Auto-Retrying / Event-Driven: 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: Appears in Trace Viewer as:
    expect(TitleText).toHaveText('Are you sure?')

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 should only have to update it in one place (the object class), rather than across fifty different test files. Check out 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 every single test. Specter handles connecting to the engine, spinning up the map, spawning the actors, and cleaning them up before the next test runs. Avoid writing massive, monolithic test scripts that share state.

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.