---
title: "Test a game from C#"
url: https://recursiveai.org/onegame/cookbook/csharp-game-tests/
description: "Write xUnit tests for a C# game with onegame’s test API: drive a headless game by id and command, run scenario files, and prove determinism with RunTwice…"
version: "1.2.0"
---

# Test a game from C#

Write xUnit tests for a C# game with onegame’s test API: drive a headless game by id and command, run scenario files, and prove determinism with RunTwice, SnapshotRestore and Replay.

`RecursiveAI.OneGame.Testing` runs the game headless inside the test process, through the same operations as scenarios and live control. `onegame test` runs these tests together with the scenarios.

## The test project

Put it in `tests/` inside the game’s folder. `onegame test` runs every project under the game folder whose name contains “Test”, with `dotnet test --project`.

**`tests/Orchard.Tests.csproj`**

```xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <OutputType>Exe</OutputType>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="../orchard.csproj" />
    <PackageReference Include="RecursiveAI.OneGame.Testing" Version="1.2.0" />
    <PackageReference Include="RecursiveAI.OneGame.Live" Version="1.2.0" />
    <PackageReference Include="xunit.v3" Version="4.0.2" />
    <Using Include="Xunit" />
  </ItemGroup>

  <ItemGroup>
    <None Include="../scenarios/**" LinkBase="scenarios" CopyToOutputDirectory="PreserveNewest" />
  </ItemGroup>

</Project>
```

Two more files in the game’s folder. The game’s project must leave the tests out of its own compile:

```xml
<Project Sdk="RecursiveAI.OneGame.Sdk/1.2.0">

  <PropertyGroup>
    <AssemblyName>Orchard</AssemblyName>
    <RootNamespace>Orchard</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <!-- The tests are their own project. -->
    <Compile Remove="tests/**" />
  </ItemGroup>

</Project>
```

And a `global.json` that selects the Microsoft.Testing.Platform runner, which `dotnet test --project` needs on the .NET 10 SDK:

```json
{
  "test": {
    "runner": "Microsoft.Testing.Platform"
  }
}
```

Without it, `onegame test` runs the scenarios, then the test project fails with `MSBUILD : error MSB1001: Unknown switch.` (`Switch: --project`), and the run exits 1.

## The tests

**`tests/TreeTests.cs`**, for the apple tree from [Getting started](https://recursiveai.org/onegame/docs/getting-started/):

```csharp
using OneGame.Testing;

namespace Orchard.Tests;

public class TreeTests
{
    private static readonly TestOptions Options = new() { Width = 640, Height = 360, Seed = 7 };

    // Pick one apple every 40 frames.
    private static void PickNow(TestClient client, int frame)
    {
        if (frame % 40 == 39)
        {
            client.Send("Tree", "Pick", "1");
        }
    }

    [Fact]
    public void PickingTakesAnAppleFromTheTree()
    {
        using var cluster = TestCluster.Start(new OrchardGame(), Options);
        var client = cluster.Client;

        client.Step(300);
        var before = int.Parse(client.Get("Tree", "Apples"));
        client.Click("pick").Step(1);

        Assert.Equal(before - 1, int.Parse(client.Get("Tree", "Apples")));
        Assert.Equal("1", client.Get("Tree", "Picked"));
    }

    [Fact]
    public void ThePickApplesScenarioPasses()
    {
        using var cluster = TestCluster.Start(new OrchardGame(), Options);
        var result = cluster.Run("scenarios/pick-apples.yaml");
        Assert.True(result.Passed, result.ToString());
    }

    [Fact]
    public void TheSameRunTwiceGivesTheSameHashEveryFrame() =>
        Determinism.RunTwice(() => new OrchardGame(), PickNow, 600, Options).ThrowIfDifferent();

    [Fact]
    public void ASnapshotRestoredHalfwayFinishesOnTheSameHashes() =>
        Determinism.SnapshotRestore(() => new OrchardGame(), PickNow, 600, splitFrame: 300, Options).ThrowIfDifferent();

    [Fact]
    public void ARecordingReplaysToTheSameHashes() =>
        Determinism.Replay(() => new OrchardGame(), PickNow, 600, Options).ThrowIfDifferent();
}
```

```shell
onegame test
```

```text
PASS pick-apples (41 ms, tick 302, hash 9a447aae324c764b)
1 passed, 0 failed
Running tests from /tmp/onegame/orchard/tests/bin/Debug/net10.0/Orchard.Tests.dll (net10.0|x64)
/tmp/onegame/orchard/tests/bin/Debug/net10.0/Orchard.Tests.dll (net10.0|x64) passed (334ms)

Test run summary: Passed!
  total: 5
  failed: 0
  succeeded: 5
  skipped: 0
  duration: 457ms
unit tests: 5 passed, 0 failed, 0 skipped
```

- `TestCluster.Start(game, options)` starts one headless node. `cluster.Client` clicks by id (`Click`), presses keys (`Press`), types (`Type`), sends commands as a player would (`Send`, run on the next tick), steps frames (`Step`) and reads one synced property as text (`Get`). `Call(op, json)` runs any live-control operation, and `Capture` renders a frame.
- `cluster.Run(path)` runs a scenario file against the game and returns its result.
- `Determinism.RunTwice` runs the script in two fresh games and compares the state hash after every frame. `SnapshotRestore` takes a snapshot at `splitFrame`, restores it into a new game and checks the rest of the run lands on the same hashes. `Replay` records the run and replays it. `ThrowIfDifferent()` fails the test with the first frame that differs and both hashes.
- `TestOptions.Seed` fixes the world’s seed; `Width`, `Height` and `Scale` set the window, and `Gpu` set to off renders captures on the CPU.

## Good to know

- `RecursiveAI.OneGame.Live` is the development tools package (scenario runs, captures, replays) that a Debug build of the game carries and the test API drives.
- `onegame test --no-unit` skips the C# tests; `dotnet test --project tests/Orchard.Tests.csproj` runs only them.
- To test servers and clients as separate processes, `TestCluster.Launch(spec)` starts a cluster from the same spec as `onegame cluster`: see [Client/server sync with a dedicated server](https://recursiveai.org/onegame/cookbook/client-server-sync/).
