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.
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 NUnit3TestAdapterWhat each command adds
| Code | Meaning |
|---|---|
dotnet add package Gua.Core | Adds shared Gua types and the native runtime boundary. |
dotnet add package Gua.Testing | Adds semantic locators, actions, waits, assertions, and diagnostics. |
dotnet add package Gua.Testing.Godot | Adds the process host that starts Godot and connects to its bridge. |
dotnet add package Microsoft.NET.Test.Sdk | Lets dotnet test discover and run a test framework. |
dotnet add package NUnit | Adds NUnit tests, fixtures, and assertions. |
dotnet add package NUnit3TestAdapter | Connects NUnit discovery to Microsoft.NET.Test.Sdk. |
If Godot is not on PATH, set its executable explicitly.
$env:GODOT_EXECUTABLE = "C:\path\to\Godot_v4.7-stable_mono_win64_console.exe"
dotnet test YourGame.Tests.csprojWhat the run commands mean
| Code | Meaning |
|---|---|
$env:GODOT_EXECUTABLE = "...console.exe" | Selects the Godot executable inherited by the test process and its child. |
dotnet test YourGame.Tests.csproj | Builds the named test project and runs its discovered NUnit tests. |
2. Launch and operate a scene semantically
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
| Code | Meaning |
|---|---|
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 = projectPath | Points the host at the directory containing project.godot. |
UseAvailableBridgePort = true | Selects a loopback port instead of sharing fixed port 8765. |
StartupResetPolicy = GuaResetPolicy.Strict | Fails rather than silently deleting dirty state at startup. |
TeardownResetPolicy = GuaResetPolicy.Strict | Reports 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
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
| Code | Meaning |
|---|---|
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
Set UseAvailableBridgePort = true to avoid collisions in parallel runs.
Detect leaked requests and events at startup and teardown.
Wait for nodes, values, or stable snapshots instead of sleeping for a fixed time.
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.
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
| Code | Meaning |
|---|---|
GodotSceneTestHost.Load("res://Main.tscn", ...) | Starts the scene and returns a host connected to its Gua bridge. |
ProjectPath = projectPath | Selects the Godot project used to resolve the scene. |
UseAvailableBridgePort = true | Asks the OS for an available port and passes it as GUA_BRIDGE_PORT. |
using var host | Closes 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.
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
| Code | Meaning |
|---|---|
StartupResetPolicy = GuaResetPolicy.Strict | Rejects dirty selected queues before the test starts. |
TeardownResetPolicy = GuaResetPolicy.Strict | Reports unconsumed work as a test leak at teardown. |
CaptureDiagnosticsBeforeTeardown = true | Captures evidence before the process is destroyed when teardown fails. |
CleanupAfterLeakReport = true | Performs 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.
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
| Code | Meaning |
|---|---|
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.
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
| Code | Meaning |
|---|---|
host.CreateDiagnosticsSession(...) | Creates an evidence collector tied to this Godot host. |
TestContext.CurrentContext.Test.FullName | Uses 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 = diagnostics | Selects 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.
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: stableWhat each workflow section controls
| Code | Meaning |
|---|---|
on: pull_request / push | Runs the workflow for pull requests and pushes to main. |
runs-on: windows-latest | Uses the Windows runner supported by the released Godot integration. |
actions/checkout@v4 | Checks out the game and test sources. |
uses: link1345/gua-tester/godot@v2.2 | Installs Godot, links the released add-on, and runs dotnet test. |
project-path: game | Points to the directory containing project.godot. |
test-project: tests/GuaTester.Tests.csproj | Selects the .NET test project to run. |
godot-version / godot-status | Pins the runner to Godot 4.7 stable. |