---
title: "Automated game testing with scenarios"
url: https://recursiveai.org/onegame/cookbook/automated-game-testing/
description: "Write game tests as YAML scenarios that click, press keys, send commands and step frames, then check state, UI, sounds and pictures, and run them headless…"
version: "1.2.0"
---

# Automated game testing with scenarios

Write game tests as YAML scenarios that click, press keys, send commands and step frames, then check state, UI, sounds and pictures, and run them headless in CI with onegame test.

A scenario is a test written as text. `onegame test` runs every file in `scenarios/` in a headless game: no window, no display and no GPU needed. This one drives the apple tree from [Getting started](https://recursiveai.org/onegame/docs/getting-started/):

```yaml
name: pick-apples
description: Apples grow, picking takes them, and picking more than there are is refused.
seed: 7
steps:
  - expect:
      ui: { id: apples, text: "0 on the tree, 0 picked" }
  - click: pick              # nothing to pick yet: refused
  - step: 300
  - click: pick
  - step: 1
  - command: { entity: Tree, name: Pick, args: [99] }   # more than there are: refused
  - step: 1
expect:
  - state: { entity: Tree, property: Picked, equals: 1 }
  - state: { entity: Tree, property: Apples, equals: 9 }
  - ui: { id: apples, text: "9 on the tree, 1 picked" }
```

```shell
onegame test
```

```text
PASS pick-apples (39 ms, tick 302, hash 9a447aae324c764b)
1 passed, 0 failed
```

## A failing test

With `equals: 0` on the `Apples` line instead, the same run reports the line, the expected value and the actual one, and exits 1:

```text
FAIL pick-apples (40 ms, tick 302, hash 9a447aae324c764b)
  expect state (line 15): Tree.Apples is 9, expected 0
    expected: 0
    actual:   9
0 passed, 1 failed
```

`onegame test --json` prints the same as one JSON object, with every step and expectation and its line, for CI to read. The exit code is 0 when everything passed and 1 when a check failed.

## What a scenario can do

The top level takes `name`, `description`, `seed`, `window` (`640x360`), `fixture` and `snapshot`, then `steps` and `expect`. Unknown keys are errors with a line number.

| Steps | |
|---|---|
| `click: pick` | an element by id; also `{selector: "Button"}` or `{x: 10, y: 20}` |
| `step: 300` | run frames |
| `command: { entity: Tree, name: Pick, args: [2] }` | send a command as a player would |
| `set: { entity: Tree, property: Apples, value: 40 }` | write one scalar of state directly |
| `press: Tab`, `type: "text"` | keys and typed text; `press: { key: d, state: down }` holds a key until `state: up` |
| `action: { name: move, state: down, x: 1, y: 0 }` | an input action, as the keyboard would press it; `action: jump` taps a button. See [Input actions](https://recursiveai.org/onegame/cookbook/input-actions/) |
| `pad: { button: south }`, `pad: { axis: leftx, value: 1 }` | the harness’s virtual gamepad |
| `nav: down`, `nav: activate` | a controller: `up down left right next previous activate cancel`, or a button by name |
| `fixture: name`, `snapshot: file`, `reset: 77` | replace the world mid-scenario |
| `assets: { reload: art/ship.png }` | reload a file the game loaded; also `assets: check` |
| `expect: ...` | check something between steps |

| Expectations | |
|---|---|
| `state: { entity: Tree, property: Apples, equals: 9 }` | a synced property; also `min`, `max`; paths like `Stock[1]` or `Stock.count` |
| `ui: { id: apples, text: "..." }` | an element’s text; also `selector` with `count`, `contains`, `visible`, `enabled`, `focused`, `pressed` |
| `sound: { name: coin, count: 1, frames: 5 }` | the game played a clip, without a sound device |
| `action: { name: boost, held: true }` | an action’s state: `held`, `pressed`, `released`, `x`, `y` |
| `asset: { path: art/ship.png, state: ready }` | a file the game loaded: `state`, `version`, `error` |
| `tick: 3` | the world’s tick |
| `capture: { reference: refs/main.png, tolerance: 2, max-diff: 0.001 }` | a picture of the screen against a reference: see [Screenshot tests](https://recursiveai.org/onegame/cookbook/screenshot-tests/) |

`entity:` is a type name (the first entity of that type), a numeric id, or `Type#n` for the nth. A step that fails stops the scenario; the expectations under `expect` are checked once every step has run, in order. An `expect:` step takes one expectation; use one step per check.

## Running a subset

```shell
onegame test --scenario scenarios/pick-apples.yaml    # one file
onegame test --no-unit                                # scenarios only, not the C# test projects
```

`onegame test` also runs any C# test project in the game’s folder whose name contains “Test”. See [Test a game from C#](https://recursiveai.org/onegame/cookbook/csharp-game-tests/).

## Good to know

- Scenarios run against the real game: the same host, screens and input path as a player, through the same operations live control uses.
- A scenario with a `seed` is repeatable to the bit: the `hash` in the result is the same on every run.
- Scenarios run in one process. To test a server and several clients over a real network, a cluster scenario adds `node:`, `wait`, `agree` and `drift`: see [Client/server sync with a dedicated server](https://recursiveai.org/onegame/cookbook/client-server-sync/).
- At 1.2.0, an action held with `state: down` stays held into the next scenario of the same `onegame test` run: the scaffold’s `steer-ship` leaves `move` and `boost` held. End a scenario by releasing what it holds (`state: up`), and start one that depends on input by releasing too.
- `onegame web test` runs scenarios in headless Chrome, but the web export needs an engine checkout at 1.2.0.

Every key is listed in [Driving a game](https://recursiveai.org/onegame/docs/driving-a-game/).

A longer guide with replays, screenshot diffs and CI: [Automated game testing](https://recursiveai.org/onegame/articles/automated-game-testing/).
