1. Add test packages
Gua.Testing.Unity is published on NuGet. These commands resolve the latest compatible stable packages; 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.UnityAlign the game integration and related Gua package versions. See version management for recording and pinning the result.
What 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 Mono Player for Auto or the selected platform. |
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 the selected desktop 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");
await name.SetValueAsync("alice");
await name.FocusAsync();
await GuaAssertions.PressKeyAsync(host.Context, "Enter");
await GuaAssertions.GetById(host.Context, "RememberMe")
.SetCheckedAsync(true);
await name.ByValue("alice").ResolveAsync();What each semantic operation does
| Code | Meaning |
|---|---|
Query(host.Context).ByRole("textbox") | Queries Unity UI through the shared textbox role. |
.Within("LoginForm") | Scopes the locator; each action resolves a current strict single 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. |
ByValue("alice").ResolveAsync() | 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.
Wait for actionable UI
Gua.Testing locators resolve a current strict single match when used. Actions wait for visible/enabled state and the advertised action, then correlated host completion. Multiple matches fail immediately. Resolution, enqueue, and completion share one timeout budget; rejected enqueue and host failure are not automatically retried.
using Gua.Testing;
var play = GuaAssertions.Query(host.Context).ByRole("button", "Play");
await play.ClickAsync(timeout: TimeSpan.FromSeconds(5));
await GuaAssertions.Query(host.Context).ById("game-screen").ResolveAsync();ResolveAsync waits for the next node to exist; add the state assertion your test requires. Locator API and timeout rules.
Compare semantic state without pixels
Add Gua.Testing.Snapshots to compare UI IDs, hierarchy, text, values, and state against reviewed JSON baselines. It works with host.Context, including a headless host. Add Gua.Testing.Visual separately when rendered pixels matter. Installation, masks, and baseline update example.
dotnet add package Gua.Testing.SnapshotsSave test evidence with Gua.Testing Trace
Trace belongs to Gua.Testing; no extra Trace package is needed. Enable it for a test scope, complete it with the test's actual outcome, and create an offline HTML report at a new output path.
using Gua.Testing;
await using var trace = new GuaTraceSession(new() {
OutputDirectory = "artifacts/traces",
SavePolicy = GuaTraceSavePolicy.Always,
});
using var scope = GuaAssertionScope.Use(new() { Trace = trace });
try {
await GuaAssertions.Query(host.Context).ById("play").ClickAsync();
await trace.CompleteAsync(GuaTraceOutcome.Passed);
} catch {
await trace.CompleteAsync(GuaTraceOutcome.Failed);
throw;
}
var report = GuaTraceReport.WriteHtml(trace.ArtifactPath, "artifacts/report.html");
// Check report.Succeeded and trace.Status separately from the test result.Trace records evidence; it does not replay the flow or change its result. Capture UI/World or Observe values explicitly when required, and handle sensitive data before recording. Trace capture APIs · Open a saved Trace in Inspector.
Support when selecting a host
Unity 6000.5.3f1 desktop Mono Players are implemented and verified on Windows, Linux, Intel macOS, and Apple Silicon macOS. Editor Play Mode is verified on Windows, Linux, and Apple M4; native Intel macOS Editor is implemented but unverified. See Unity APIs and limits.