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.
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.
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, andtoBeEnabled. - 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 == trueon the Character Movement Component. - Good: Checking if the character possesses the
Status.StunnedGameplay 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.