GGua 日本語リファレンス
英語版が正式な参照です。内容に差異がある場合は、英語版を優先してください。
GODOT / TESTING

テストの仕方

Godotプロセスを起動し、ライブなSemantic UI Treeへ接続します。座標ではなくrole・text・idで操作結果を待ちます。

1. 必須NuGetパッケージを入れる

Godotの外部テストにはGua.Core、Gua.Testing、Gua.Testing.Godotの3パッケージが必要です。テストプロジェクトのディレクトリで次を実行して互換性のある最新安定版を取得し、projectへ書き込まれたversionをcommitします。

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

# NUnitを使う場合
dotnet add package Microsoft.NET.Test.Sdk
dotnet add package NUnit
dotnet add package NUnit3TestAdapter

ゲーム側と関連Guaパッケージの版を揃えます。取得後の版の記録・固定はバージョンの管理を参照してください。

コマンドごとの役割

コマンド追加される機能
dotnet add package Gua.CoreGuaの共通型とnative runtime境界をテストprojectへ追加します。
dotnet add package Gua.TestingSemantic locator、action、待機、assertion、diagnosticsを追加します。
dotnet add package Gua.Testing.GodotGodot processの起動、bridge接続、終了処理を行うtest hostを追加します。
dotnet add package Microsoft.NET.Test.Sdkdotnet testからtest runnerを起動する基盤です。
dotnet add package NUnit[Test]やAssertなどNUnitのtest APIを追加します。
dotnet add package NUnit3TestAdapterMicrosoft.NET.Test.SdkからNUnit testを検出・実行できるようにします。

GodotがPATHにない場合は環境変数を設定します。

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

実行コマンドの意味

記述意味
$env:GODOT_EXECUTABLE = "...console.exe"このPowerShell processと子processに、test hostが起動すべきGodot実行ファイルを指定します。
dotnet test YourGame.Tests.csproj指定したtest projectをbuildし、検出されたNUnit testを実行します。

2. シーンを起動して意味で操作

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..."
);

テストコードを処理順に読む

記述意味
using Gua.Testing;locator、action、待機、assertion APIを読み込みます。
using Gua.Testing.Godot;Godot process用test hostとoption型を読み込みます。
using var host = GodotSceneTestHost.Load(...)Main.tscnを別Godot processで開き、bridge接続完了まで待ちます。usingによりtest終了時にprocessと接続を破棄します。
"res://Main.tscn"Godot project内で起動するsceneのresource pathです。
ProjectPath = projectPathproject.godotがあるdirectoryをtest hostへ伝えます。
UseAvailableBridgePort = true空いているloopback portを選び、並列testで8765が衝突しにくくします。
StartupResetPolicy = GuaResetPolicy.Stricttest開始時に前sessionの未処理requestやeventがあれば、黙って消さず失敗させます。
TeardownResetPolicy = GuaResetPolicy.Stricttest終了時にも処理漏れを検査し、別testへの持ち越しを検出します。
GetByRole(..., "button", "Start Game")座標ではなくrole=buttonかつ名前=Start GameのControlを検索します。
.ToBeVisible()そのControlが現在のsnapshotで表示状態だと確認します。
GetById(..., "start").ClickAsync()固定ID startへclick要求を送り、Godot adapterから同じrequest IDの完了結果が返るまで待ちます。
WaitForTextAsync(..., "loading", "Loading...")最新snapshotを再取得しながら、ID loadingのtextがLoading...になるまで待ちます。

3. フォーム操作

非同期アクションcsharp
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");

フォーム操作の意味

記述意味
GuaAssertions.Query(host.Context)接続中のGodot UI Treeに対するSemantic queryを組み立てます。
.ByRole("textbox")候補をtextbox roleに絞ります。
.Within("LoginForm")ID LoginFormの子孫だけを検索範囲にします。
.Get()候補がちょうど1件ならlocatorを返し、0件または複数なら曖昧さを報告します。
name.SetValueAsync("alice")textboxへ値変更を要求し、hostの処理完了まで待ちます。
name.FocusAsync()同じtextboxへfocus actionを送り、次のglobal key入力先にします。
PressKeyAsync(host.Context, "Enter")現在focus中のControlへEnterのkey-down/key-upを配送します。
GetById(..., "RememberMe").SetCheckedAsync(true)checkboxを固定IDで取得し、checked=trueへ変更します。
name.WaitForValueAsync("alice")action完了だけで終えず、公開されたvalueが実際にaliceになるまで待ちます。

安定したテストにする4原則

空きポートを使う

UseAvailableBridgePort = trueで並列テストの衝突を避けます。

厳格にリセット

開始・終了時のStrict resetで未消費要求やイベント漏れを検出します。

待機を使う

固定sleepではなく、状態・値・安定スナップショットを待ちます。

失敗を保存する

UI Tree、ログ、stdout/stderr、任意のPNGを診断成果物へ残します。

1. テストごとに空きポートを確保する

固定の8765を共有すると、並列実行時に一方のGodotだけがポートを取得します。ホストに空きポートを予約させ、子プロセスへGUA_BRIDGE_PORTとして渡します。

空きポートでシーンを起動csharp
using var host = GodotSceneTestHost.Load(
    "res://Main.tscn",
    new GodotSceneTestHostOptions
    {
        ProjectPath = projectPath,
        UseAvailableBridgePort = true,
    });

// 予約されたURLは子プロセスとRemoteContextへ自動設定されます。

空きポート設定の意味

記述意味
GodotSceneTestHost.Load("res://Main.tscn", ...)指定sceneを起動し、Gua bridgeへ接続したhostを返します。
ProjectPath = projectPathsceneを解決するGodot projectのdirectoryです。
UseAvailableBridgePort = trueOSに空きportを選ばせ、GUA_BRIDGE_PORTとしてGodot processへ渡します。
using var hostscope終了時に接続とGodot processを確実に終了します。

2. 開始前と終了前にStrict resetを行う

前のテストが残したノード・要求・イベントを次のテストへ持ち込みません。終了時に未消費の要求やイベントがあれば、黙って削除せずテストを失敗させます。

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,
    });

Strict isolation optionの意味

option意味
StartupResetPolicy = GuaResetPolicy.Strict開始時に選択対象のqueueがdirtyならresetせず失敗します。
TeardownResetPolicy = GuaResetPolicy.Strict終了時の未消費request/eventをtest leakとして報告します。
CaptureDiagnosticsBeforeTeardown = true終了検査に失敗した場合、processを破棄する前にUI Treeやlogを保存します。
CleanupAfterLeakReport = trueleakの証拠を取得した後だけ非strict cleanupを行い、後続処理を妨げる残留状態を除きます。

3. Sleepではなく観測したい状態を待つ

Task.Delay(1000)では、速い環境で時間を無駄にし、遅い環境では不足します。操作後に期待するノード状態を直接待ちます。

状態ベースの待機csharp
await GuaAssertions.GetById(host.Context, "start")
    .ClickAsync();

// 表示されるまで最新スナップショットを再取得します。
await GuaAssertions.WaitForVisibleAsync(
    host.Context,
    "loading",
    timeout: TimeSpan.FromSeconds(3),
    pollInterval: TimeSpan.FromMilliseconds(20));

// 描画後の状態が3フレーム変化しないことも確認できます。
await GuaAssertions.WaitForStableSnapshotAsync(
    host.Context,
    stableFrames: 3);

状態待機の意味

記述意味
ClickAsync()click要求のrequest IDに対応するhost完了結果まで待ちます。画面変化そのものの完了ではありません。
WaitForVisibleAsync(..., "loading", ...)ID loadingがvisibleになるまで新しいsnapshotをpollします。
timeout: TimeSpan.FromSeconds(3)3秒以内に条件を満たさなければtimeoutとして失敗します。
pollInterval: TimeSpan.FromMilliseconds(20)snapshotを再確認する間隔です。固定sleep後の一度きりの確認とは異なります。
WaitForStableSnapshotAsync(..., stableFrames: 3)Semantic snapshotが3frame連続で変化しないことを待ち、遷移途中を避けます。

4. アサーション失敗時の診断成果物を設定する

失敗時に最終UI Tree、操作履歴、ログ、Godotのstdout/stderrをartifacts/guaへ保存します。CIではこのディレクトリをテスト成果物としてアップロードします。

失敗診断を有効化csharp
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,
    });

// 失敗すると、主例外を保ったまま診断ファイルが保存されます。
GuaAssertions.GetByRole(
    host.Context, "button", "Start Game"
).ToBeVisible();

診断設定の意味

記述意味
host.CreateDiagnosticsSession(...)このGodot hostに紐づく失敗証拠の収集sessionを作ります。
TestContext.CurrentContext.Test.FullNameNUnitの完全test名をartifact directoryの識別に使います。
Path.Combine(..., "artifacts", "gua")OS依存のseparatorを正しく使って出力先を組み立てます。
GuaAssertionScope.Use(...)このscope内で失敗したGua assertionにdiagnostics sessionを関連付けます。
DiagnosticsSession = diagnosticsassertion失敗時にUI Tree、履歴、log、process情報を保存する対象を指定します。
.ToBeVisible()失敗した場合も元のassertion例外を主原因として保ち、診断取得失敗は副次情報にします。

GitHub Actionsを使う場合は、テスト後にartifacts/gua/**をactions/upload-artifactへ渡すと、失敗した実行から診断ファイルを取得できます。

GitHub Actionsで実行する

以下の例はgua-v1.1.1のアドオンを導入します。テストプロジェクトのNuGetも1.1.1に揃えます。CIの導入方法とバージョン指定を参照してください。

公開Actionのgua-project/gua-tester/godot@v3.2を使うと、runnerに合うGodot公式版の導入、選択したReleaseのGuaアドオンの配置、GODOT_EXECUTABLEの設定、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: gua-project/gua-tester/godot@v3.2
        with:
          project-path: game
          test-project: tests/GuaTester.Tests.csproj
          godot-version: "4.7"
          gua-plugin-tag: gua-v1.1.1
          godot-status: stable

workflowの各部

記述意味
on: pull_request / pushpull requestとmainへのpushをtestの起動条件にします。
runs-on: windows-latestここではWindowsを例示します。ubuntu-22.04/macOS runnerでは対応するGodot公式archiveを自動選択します。
actions/checkout@v4gameとtestのsourceをrunnerへcheckoutします。
uses: gua-project/gua-tester/godot@v3.2Godot導入、Gua add-on配置、dotnet testをまとめた公開Actionを実行します。
project-path: gameproject.godotがあるdirectoryを指定します。
test-project: tests/GuaTester.Tests.csproj実行する.NET test projectを指定します。
godot-version / godot-status取得するGodotを4.7 stableへ固定します。

操作できるUIを待つ

Gua.Testingのlocatorは使用時に最新のtreeから厳密な単一一致を解決します。actionはvisible/enabledと対応actionを待ち、requestIdに対応するhost completionを待ちます。複数一致は即時失敗です。解決・enqueue・completionは同じtimeoutを共有し、enqueue拒否やhost失敗は自動再試行しません。

クリック後に次の画面を待つcsharp
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は次のnodeの存在を待ちます。テストで必要な状態のassertionも加えます。locator APIとtimeout規則。

ピクセルを使わず意味的な状態を比較する

Gua.Testing.Snapshotsを追加すると、UIのID、階層、text、value、stateをreview済みJSON baselineと比較できます。headlessでもhost.Contextで使えます。描画結果も調べる場合はGua.Testing.Visualを別途追加します。導入・mask・baseline更新の例を参照してください。

意味的な比較を追加するpowershell
dotnet add package Gua.Testing.Snapshots

Gua.Testing Traceでテストの証拠を保存する

TraceはGua.Testingの機能です。追加のTraceパッケージは不要です。テストscopeに設定し、実際の結果で完了させ、新しい出力先にoffline HTML reportを生成します。

テストを記録して確認するcsharp
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は証拠を記録し、flowの再生や結果の変更はしません。UI/WorldやObserve値が必要な場合は明示的に取得し、記録前に機密情報の扱いを決めます。Traceの取得API・Inspectorで保存済みTraceを開くを参照してください。