GGua REFERENCE日本語GitHub ↗
GODOT / TESTING

Testing UI

Launch a Godot process and connect to its live semantic UI tree. Locate controls by role, text, and ID, then wait for observable results instead of clicking coordinates.

1. Install the required NuGet packages

External Godot tests require all three packages: Gua.Core, Gua.Testing, and Gua.Testing.Godot. Run these commands in the test project directory to resolve the latest stable releases, then commit the versions written to the project.

PowerShellpowershell
dotnet add package Gua.Core
dotnet add package Gua.Testing
dotnet add package Gua.Testing.Godot

# When using NUnit
dotnet add package Microsoft.NET.Test.Sdk
dotnet add package NUnit
dotnet add package NUnit3TestAdapter

What each command adds

CodeMeaning
dotnet add package Gua.CoreAdds shared Gua types and the native runtime boundary.
dotnet add package Gua.TestingAdds semantic locators, actions, waits, assertions, and diagnostics.
dotnet add package Gua.Testing.GodotAdds the process host that starts Godot and connects to its bridge.
dotnet add package Microsoft.NET.Test.SdkLets dotnet test discover and run a test framework.
dotnet add package NUnitAdds NUnit tests, fixtures, and assertions.
dotnet add package NUnit3TestAdapterConnects NUnit discovery to Microsoft.NET.Test.Sdk.

If Godot is not on PATH, set its executable explicitly.

PowerShellpowershell
$env:GODOT_EXECUTABLE = "C:\path\to\Godot_v4.7-stable_mono_win64_console.exe"
dotnet test YourGame.Tests.csproj

What the run commands mean

CodeMeaning
$env:GODOT_EXECUTABLE = "...console.exe"Selects the Godot executable inherited by the test process and its child.
dotnet test YourGame.Tests.csprojBuilds the named test project and runs its discovered NUnit tests.

2. Launch and operate a scene semantically

TitleScreenTests.cscsharp
using Gua.Testing;
using Gua.Testing.Godot;

using var host = GodotSceneTestHost.Load(
    "res://Main.tscn",
    new GodotSceneTestHostOptions {
        ProjectPath = projectPath,
        UseAvailableBridgePort = true,
        StartupResetPolicy = GuaResetPolicy.Strict,
        TeardownResetPolicy = GuaResetPolicy.Strict,
    });

GuaAssertions.GetByRole(
    host.Context, "button", "Start Game"
).ToBeVisible();

await GuaAssertions.GetById(
    host.Context, "start"
).ClickAsync();

await GuaAssertions.WaitForTextAsync(
    host.Context, "loading", "Loading..."
);

Read the test in execution order

CodeMeaning
using Gua.Testing;Imports locators, actions, waits, and assertions.
using Gua.Testing.Godot;Imports the Godot process host and its options.
using var host = GodotSceneTestHost.Load(...)Starts Main.tscn in a child Godot process and waits for the bridge; using disposes both.
"res://Main.tscn"Selects the scene resource inside the Godot project.
ProjectPath = projectPathPoints the host at the directory containing project.godot.
UseAvailableBridgePort = trueSelects a loopback port instead of sharing fixed port 8765.
StartupResetPolicy = GuaResetPolicy.StrictFails rather than silently deleting dirty state at startup.
TeardownResetPolicy = GuaResetPolicy.StrictReports leaked requests or events at the end of the test.
GetByRole(..., "button", "Start Game")Finds a button by semantic role and accessible name, not coordinates.
.ToBeVisible()Asserts that the matched node is visible in the current snapshot.
GetById(..., "start").ClickAsync()Sends a click and waits for the request-correlated host completion.
WaitForTextAsync(..., "loading", "Loading...")Refreshes snapshots until node loading exposes the expected text.

3. Operate forms

Correlated asynchronous actionscsharp
var name = GuaAssertions.Query(host.Context)
    .ByRole("textbox").Within("LoginForm").Get();

await name.SetValueAsync("alice");
await name.FocusAsync();
await GuaAssertions.PressKeyAsync(host.Context, "Enter");
await GuaAssertions.GetById(host.Context, "RememberMe")
    .SetCheckedAsync(true);
await name.WaitForValueAsync("alice");

What each form operation does

CodeMeaning
GuaAssertions.Query(host.Context)Starts a semantic query against the connected UI tree.
.ByRole("textbox")Limits candidates to textbox nodes.
.Within("LoginForm")Searches only descendants of node LoginForm.
.Get()Requires exactly one match and reports zero or ambiguous results.
name.SetValueAsync("alice")Requests a value change and waits for host completion.
name.FocusAsync()Focuses the textbox so a global key action has a target.
PressKeyAsync(host.Context, "Enter")Sends an Enter key-down/key-up gesture to the focused Control.
GetById(..., "RememberMe").SetCheckedAsync(true)Finds the checkbox by stable ID and checks it.
name.WaitForValueAsync("alice")Observes the published value instead of treating action completion as state completion.

Four rules for reliable tests

Use an available port

Set UseAvailableBridgePort = true to avoid collisions in parallel runs.

Reset strictly

Detect leaked requests and events at startup and teardown.

Wait for state

Wait for nodes, values, or stable snapshots instead of sleeping for a fixed time.

Keep failure evidence

Capture the UI tree, logs, stdout/stderr, and optional PNG evidence.

1. Reserve a port per test

If every test uses 8765, only one Godot process can bind it during a parallel run. Let the host reserve a loopback port and pass it to the child as GUA_BRIDGE_PORT.

Launch on an available portcsharp
using var host = GodotSceneTestHost.Load(
    "res://Main.tscn",
    new GodotSceneTestHostOptions
    {
        ProjectPath = projectPath,
        UseAvailableBridgePort = true,
    });

// The reserved URL is assigned to the child and RemoteContext.

What the port options do

CodeMeaning
GodotSceneTestHost.Load("res://Main.tscn", ...)Starts the scene and returns a host connected to its Gua bridge.
ProjectPath = projectPathSelects the Godot project used to resolve the scene.
UseAvailableBridgePort = trueAsks the OS for an available port and passes it as GUA_BRIDGE_PORT.
using var hostCloses the connection and child process when the scope ends.

2. Reset strictly before and after the test

Do not carry nodes, requests, or events from one test into the next. A strict teardown reports unconsumed work instead of silently deleting it.

Strict isolationcsharp
using var host = GodotSceneTestHost.Load(
    "res://Main.tscn",
    new GodotSceneTestHostOptions
    {
        ProjectPath = projectPath,
        UseAvailableBridgePort = true,
        StartupResetPolicy = GuaResetPolicy.Strict,
        TeardownResetPolicy = GuaResetPolicy.Strict,
        CaptureDiagnosticsBeforeTeardown = true,
        CleanupAfterLeakReport = true,
    });

What strict isolation changes

CodeMeaning
StartupResetPolicy = GuaResetPolicy.StrictRejects dirty selected queues before the test starts.
TeardownResetPolicy = GuaResetPolicy.StrictReports unconsumed work as a test leak at teardown.
CaptureDiagnosticsBeforeTeardown = trueCaptures evidence before the process is destroyed when teardown fails.
CleanupAfterLeakReport = truePerforms cleanup only after preserving the leak report.

3. Wait for observed state, not elapsed time

Task.Delay(1000) wastes time on a fast machine and may still be too short on a slow one. Wait directly for the state that should follow the action.

State-based synchronizationcsharp
await GuaAssertions.GetById(host.Context, "start")
    .ClickAsync();

// Re-fetch snapshots until the node is visible.
await GuaAssertions.WaitForVisibleAsync(
    host.Context,
    "loading",
    timeout: TimeSpan.FromSeconds(3),
    pollInterval: TimeSpan.FromMilliseconds(20));

// Optionally require three unchanged rendered frames.
await GuaAssertions.WaitForStableSnapshotAsync(
    host.Context,
    stableFrames: 3);

What the waits guarantee

CodeMeaning
ClickAsync()Waits for the correlated click result, not the full screen transition.
WaitForVisibleAsync(..., "loading", ...)Polls fresh snapshots until loading is visible.
timeout: TimeSpan.FromSeconds(3)Fails if the condition is still false after three seconds.
pollInterval: TimeSpan.FromMilliseconds(20)Controls how often the state is checked.
WaitForStableSnapshotAsync(..., stableFrames: 3)Waits for three consecutive unchanged semantic frames.

4. Capture diagnostics when an assertion fails

Write the final UI tree, operation history, logs, and Godot stdout/stderr to artifacts/gua. In CI, upload that directory as a test artifact.

Enable failure diagnosticscsharp
using var diagnostics = host.CreateDiagnosticsSession(
    TestContext.CurrentContext.Test.FullName,
    outputDirectory: Path.Combine(
        TestContext.CurrentContext.WorkDirectory,
        "artifacts", "gua"));

using var assertionScope = GuaAssertionScope.Use(
    new GuaAssertionOptions
    {
        DiagnosticsSession = diagnostics,
    });

// A failure preserves the primary exception and writes diagnostics.
GuaAssertions.GetByRole(
    host.Context, "button", "Start Game"
).ToBeVisible();

What the diagnostics setup does

CodeMeaning
host.CreateDiagnosticsSession(...)Creates an evidence collector tied to this Godot host.
TestContext.CurrentContext.Test.FullNameUses the full NUnit test name to identify the artifact set.
Path.Combine(..., "artifacts", "gua")Builds a portable output path.
GuaAssertionScope.Use(...)Associates assertion failures in this scope with the diagnostics session.
DiagnosticsSession = diagnosticsSelects the UI tree, history, logs, and process evidence collector.
.ToBeVisible()Keeps the assertion as the primary failure even if evidence capture also fails.

With GitHub Actions, pass artifacts/gua/** to actions/upload-artifact after the test step so evidence remains available from a failed run.

Run tests with GitHub Actions

The public link1345/gua-tester/godot@v2.2 action installs Godot on a Windows runner, links the latest released Gua add-on, sets GODOT_EXECUTABLE, and runs dotnet test.

.github/workflows/godot.ymlyaml
name: Godot Gua UI tests

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Gua UI tests
        uses: link1345/gua-tester/godot@v2.2
        with:
          project-path: game
          test-project: tests/GuaTester.Tests.csproj
          godot-version: "4.7"
          godot-status: stable

What each workflow section controls

CodeMeaning
on: pull_request / pushRuns the workflow for pull requests and pushes to main.
runs-on: windows-latestUses the Windows runner supported by the released Godot integration.
actions/checkout@v4Checks out the game and test sources.
uses: link1345/gua-tester/godot@v2.2Installs Godot, links the released add-on, and runs dotnet test.
project-path: gamePoints to the directory containing project.godot.
test-project: tests/GuaTester.Tests.csprojSelects the .NET test project to run.
godot-version / godot-statusPins the runner to Godot 4.7 stable.