Skip to content

Writing engine tests ​

A test that uses Godot often needs more than a method call: a timer needs frames, a signal needs a listener, and a queued deletion needs time to finish. The testing helpers handle these steps on the engine's thread.

The examples use godot for the shared fixture:

FrameworkFixtureHelpers namespace
xUnitConstructor argument godottwodog.Testing.Xunit
NUnitUse var godot = EngineFixture; inside a testtwodog.Testing.NUnit

Wait for frames ​

An ordinary await Task.Delay(...) does not advance Godot. Choose a helper when the work needs the main loop:

HelperWaits for
godot.WaitUntilAsync(() => condition)A condition to become true
godot.AwaitAsync(task)A task to finish; returns its result or propagates its failure
signal.WaitAsync(godot)A recorded signal emission
GodotAssert.FreedAsync(godot, node)A native object to be deleted

For example, wrap an operation that awaits a Godot signal:

csharp
async Task NextFrame()
{
    await godot.Tree.ToSignal(godot.Tree, SceneTree.SignalName.ProcessFrame);
}

await godot.AwaitAsync(NextFrame());
await godot.WaitUntilAsync(() => node.IsNodeReady(),
    timeout: TimeSpan.FromSeconds(2));

Timeouts and cancellation

Waits default to five seconds and observe your test framework's cancellation token. You can pass timeout: and cancellationToken: to override or extend those defaults. A timeout fails the test but cannot cancel a task you supplied; cancel or clean up that work yourself.

Await one pumping operation at a time. Keep Godot access on the fixture's thread, including after await; avoid Task.Run and ConfigureAwait(false).

Check a signal ​

Subscribe before you trigger the behavior, so synchronous signals are caught too. Then wait if needed and assert the count:

csharp
using var timeout = GodotAssert.ExpectSignal(timer, global::Godot.Timer.SignalName.Timeout);
timer.Start();
await timeout.WaitAsync(godot);
timeout.AssertEmitted();
CallMeaning
AssertEmitted()Exactly one emission
AssertEmitted(0)No emissions
WaitAsync(godot, count: 2)Pump until at least two emissions arrive
ExpectSignal<T>(source, signal)Record a single argument per emission in Values
csharp
using var entered = GodotAssert.ExpectSignal<Node>(parent, Node.SignalName.ChildEnteredTree);
parent.AddChild(child);
entered.AssertEmitted();
var receivedChild = entered.Values[0];

Expectations disconnect on disposal, timeout, or cancellation. Disposal does not assert. A wait fails if the source is freed before the expected signal. For signals with several arguments, connect a typed callable and use WaitUntilAsync on the values you record.

Clean up native objects ​

Free nodes in finally so cleanup still happens when an assertion fails. Disposing a node's C# wrapper does not free the native node. Dispose resources and signal expectations that your test owns too.

QueueFree() schedules deletion. To check that it actually happened:

csharp
node.QueueFree();
await GodotAssert.FreedAsync(godot, node);

Tree exit and deletion are separate

TreeExiting fires while a node is still in the tree; TreeExited fires after removal. Removing a node from its parent does not free it.

Expect Godot errors ​

Unconsumed Godot errors and warnings fail engine tests, including exceptions Godot catches in C# callbacks. If a test intentionally triggers an error, consume it and check its text:

csharp
player.Health = -1;
godot.Errors.Expect("Health must not be negative");

Errors from startup or deferred work between tests fail the next test on that fixture. Both integrations also check remaining reports after engine shutdown. xUnit can report late cleanup errors as collection failures. Tests without a 2dog fixture are not checked.

Opting out of automatic error checks

Prefer expecting a known error so unrelated failures still surface.

FrameworkOpt out
xUnitMark a test or class [AllowGodotErrors], or set <TwoDogFailOnGodotErrors>false</TwoDogFailOnGodotErrors> in the test project
NUnitMark a test or class [AllowGodotErrors], or override FailOnGodotErrors to return false for the whole fixture

AllowGodotErrors clears reports after the allowed test. Disabling automatic checks for the whole project or fixture leaves the log available for inspection.

Run locally or in CI ​

bash
dotnet test MyGame.xunit
dotnet test MyGame.nunit -c Release
dotnet test MyGame.xunit --filter "FullyQualifiedName~SceneTests"
dotnet test MyGame.nunit --logger "console;verbosity=detailed"

Resource import runs automatically when the test project builds. For Editor APIs, choose -c Editor and check your xUnit or NUnit settings.

Use a headless fixture in CI, with a dummy audio driver:

yaml
- name: Run tests
  run: dotnet test MyGame.xunit --configuration Release
  env:
    GODOT_AUDIO_DRIVER: Dummy