1. Add test packages
Gua.Testing.Unity is published on NuGet. These commands resolve the latest stable releases; commit the versions written to the project and keep the three Gua packages on the same version line.
dotnet add package Gua.Core
dotnet add package Gua.Testing
dotnet add package Gua.Testing.UnityWhat each package adds
| Code | Meaning |
|---|---|
dotnet add package Gua.Core | Adds shared Gua types and the native runtime boundary. |
dotnet add package Gua.Testing | Adds the same semantic locators, actions, waits, assertions, and diagnostics used with Godot. |
dotnet add package Gua.Testing.Unity | Adds Unity Editor/Player build, startup, bridge connection, and teardown. |
2. Start a Player or Editor
using Gua.Core;
using Gua.Testing;
using Gua.Testing.Unity;
var player = UnityPlayerBuilder.Build(@"game/Assets/Scenes/Title.unity");
using var host = UnitySceneTestHost.LoadRenderedPlayer(
player, UnitySceneTestHostOptions.StrictIsolation);
GuaAssertions.GetByRole(
host.Context, "button", "Start Game"
).ToBeVisible();
await GuaAssertions.GetById(
host.Context, "start"
).ClickAsync();
await GuaAssertions.WaitForTextAsync(
host.Context, "loading", "Loading..."
);
var screenshot = host.CaptureScreenshot();Read the Unity test in execution order
| Code | Meaning |
|---|---|
using Gua.Core; | Imports shared runtime types such as reset policies. |
using Gua.Testing; | Imports semantic locators, actions, waits, and assertions. |
using Gua.Testing.Unity; | Imports the Unity Player builder and scene host. |
UnityPlayerBuilder.Build("...Title.unity") | Runs Unity Editor in batch mode and builds a temporary Windows x64 Mono Player. |
LoadRenderedPlayer(player, StrictIsolation) | Starts a rendered Player, connects to its bridge, and checks isolation at startup and teardown. |
GetByRole(..., "button", "Start Game") | Finds the button semantically across UI Toolkit, uGUI, or TMP. |
.ToBeVisible() | Asserts visibility in the current semantic snapshot. |
GetById(..., "start").ClickAsync() | Clicks the stable ID and waits for Unity listener completion. |
WaitForTextAsync(..., "loading", "Loading...") | Refreshes snapshots until the game publishes the expected result. |
host.CaptureScreenshot() | Requests a PNG after a later rendered frame. |
using var host | Closes the bridge and child process when the scope ends. |
| Entry point | Use |
|---|---|
LoadPlayer | Existing -batchmode -nographics Player for fast semantic tests |
LoadRenderedPlayer | Rendered existing Player for screenshots and visual tests |
LoadEditor | Open a scene in Editor Play Mode |
BuildAndLoadPlayer | Build and start a Windows x64 Mono Player |
3. Use the same semantic API as Godot
host.Context is the engine-neutral IGuaContext. Tests use the same locators and waits instead of branching on Unity UI technology.
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 semantic operation does
| Code | Meaning |
|---|---|
Query(host.Context).ByRole("textbox") | Queries Unity UI through the shared textbox role. |
.Within("LoginForm").Get() | Scopes the query and requires exactly one match. |
SetValueAsync("alice") | Sets a UI Toolkit, uGUI, or TMP text input through one action. |
FocusAsync() | Uses Unity focus so the next global key gesture has a target. |
PressKeyAsync(..., "Enter") | Sends key-down and key-up to the focused textbox. |
SetCheckedAsync(true) | Checks a UI Toolkit or uGUI/TMP toggle. |
WaitForValueAsync("alice") | Waits for a later published frame to expose the value. |
Four rules for reliable Unity tests
Use headless for semantic tests and a rendered Player or Editor for screenshots.
Observe nodes, text, and values instead of guessing Unity frame timing with sleeps.
Use available ports and strict reset to detect collisions and leaked work.
Retain the UI tree, history, Unity log, process data, and optional PNG.
Visual comparison and recording
Gua.Testing.Visual and Gua.Testing.Recording are engine-neutral packages, so both work with the host.Context supplied by Gua.Testing.Unity. Use LoadRenderedPlayer or Editor Play Mode when Visual needs a PNG. Recording operates on semantic actions and also works with a headless Player.
using Gua.Testing.Recording;
using Gua.Testing.Visual;
var comparison = await GuaVisualAssertions.ExpectScreenshotAsync(
host.Context, "title-screen", new() {
BaselineDirectory = "baselines",
ArtifactDirectory = "artifacts/gua",
});
var recorder = new GuaRecorder(host.Context);
await recorder.ClickAsync(new(Id: "start"));What the Visual and Recording calls do
| Code | Meaning |
|---|---|
using Gua.Testing.Visual; | Imports engine-neutral PNG baseline comparison. |
ExpectScreenshotAsync(host.Context, "title-screen", ...) | Captures Unity and compares it with the reviewed title-screen baseline. |
BaselineDirectory = "baselines" | Selects the version-controlled expected images. |
ArtifactDirectory = "artifacts/gua" | Stores expected, actual, diff, and manifest on mismatch. |
new GuaRecorder(host.Context) | Records semantic operations sent to the same Unity context. |
recorder.ClickAsync(new(Id: "start")) | Performs and records a request-correlated click step. |
Parallel isolation
UseAvailableBridgePort defaults to true. The host selects a loopback port and passes it as GUA_BRIDGE_PORT, avoiding a fixed collision on port 8765. It closes the listener before Unity binds, so a small selection-to-bind race remains under extreme concurrency.
var options = new UnitySceneTestHostOptions {
UnityExecutablePath = @"C:/Program Files/Unity/Hub/Editor/6000.5.3f1/Editor/Unity.exe",
ProjectPath = @"C:/src/game",
UseAvailableBridgePort = true,
TeardownResetPolicy = GuaResetPolicy.Strict,
CaptureDiagnosticsBeforeTeardown = true,
};What each Unity host option means
| Code | Meaning |
|---|---|
UnityExecutablePath | Selects Unity.exe explicitly before environment and Hub discovery. |
ProjectPath | Points to the Unity root containing Assets and ProjectSettings. |
UseAvailableBridgePort = true | Selects a loopback port and passes it as GUA_BRIDGE_PORT. |
TeardownResetPolicy = Strict | Reports unconsumed requests and events instead of deleting them. |
CaptureDiagnosticsBeforeTeardown = true | Captures evidence before terminating Unity when teardown fails. |
Diagnostics
CreateDiagnosticsSession() adds the single -logFile Unity log, PID, bridge URL, and log path to shared artifacts. stdout and stderr are not redirected separately. Strict teardown reports leaked requests or events instead of deleting them silently.
using var diagnostics = host.CreateDiagnosticsSession(
TestContext.CurrentContext.Test.FullName,
outputDirectory: "artifacts/gua",
captureScreenshot: true);
using var assertionScope = GuaAssertionScope.Use(
new GuaAssertionOptions {
DiagnosticsSession = diagnostics,
});
GuaAssertions.GetByRole(
host.Context, "button", "Start Game"
).ToBeVisible();What the diagnostics setup does
| Code | Meaning |
|---|---|
host.CreateDiagnosticsSession(...) | Adds Unity process logs and metadata to shared Gua diagnostics. |
TestContext...Test.FullName | Uses the full NUnit test name as the artifact identity. |
outputDirectory: "artifacts/gua" | Selects the evidence directory. |
captureScreenshot: true | Adds an on-demand PNG; use a rendered host. |
GuaAssertionScope.Use(...) | Associates failures in the scope with this diagnostics session. |
.ToBeVisible() | Keeps the assertion as the primary error while collecting evidence. |
SceneTimeout currently serves mainly as the default screenshot timeout; it does not wait for scene transitions automatically.