Asteroid.GodotUnitTest.TestAdapter 0.7.4

dotnet add package Asteroid.GodotUnitTest.TestAdapter --version 0.7.4
                    
NuGet\Install-Package Asteroid.GodotUnitTest.TestAdapter -Version 0.7.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Asteroid.GodotUnitTest.TestAdapter" Version="0.7.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Asteroid.GodotUnitTest.TestAdapter" Version="0.7.4" />
                    
Directory.Packages.props
<PackageReference Include="Asteroid.GodotUnitTest.TestAdapter" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Asteroid.GodotUnitTest.TestAdapter --version 0.7.4
                    
#r "nuget: Asteroid.GodotUnitTest.TestAdapter, 0.7.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Asteroid.GodotUnitTest.TestAdapter@0.7.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Asteroid.GodotUnitTest.TestAdapter&version=0.7.4
                    
Install as a Cake Addin
#tool nuget:?package=Asteroid.GodotUnitTest.TestAdapter&version=0.7.4
                    
Install as a Cake Tool

Asteroid.GodotUnitTest

Asteroid.GodotUnitTest is a minimal Godot C# test API plus VSTest adapter. It lets a Godot C# project expose tests to dotnet test while executing the test bodies inside a Godot process.

The current scope is intentionally small: discover tests, run them through Godot, and report pass/fail/skip results back to VSTest. Version 0.6.0 is a 0.x release intended for internal projects and early users. Core workflows should be repeatable, but 1.0-level API compatibility is not promised yet.

Supported versions

Area Supported Notes
Godot Godot 4.7.2 Mono Other Godot 4.x Mono versions may work but are not part of the current guarantee.
OS Windows Linux and macOS are future validation targets.
.NET development and CI .NET SDK 10, .NET runtime 8 SDK policy follows the host AsteroidFrameworkDevelop repository.
Projects net8.0 Both API and adapter projects target net8.0 (consumer compatibility surface).

Distribution is NuGet: both packages (Asteroid.GodotUnitTest, Asteroid.GodotUnitTest.TestAdapter) are published to official nuget.org from the tooling/Utility/GodotUnitTest module of the AsteroidFrameworkDevelop repository. The module is a standalone development-time tool with no dependency on the framework libraries. The historical 0.5.0 packages remain on the Asteroid Gitea NuGet source as anchors.

Quick start

Install the API, adapter, VSTest SDK, and NUnit assertions in a Godot C# project:

dotnet add package Microsoft.NET.Test.Sdk
dotnet add package NUnit
dotnet add package Asteroid.GodotUnitTest --version 0.6.0
dotnet add package Asteroid.GodotUnitTest.TestAdapter --version 0.6.0

Mark the Godot project as a test project:

<PropertyGroup>
  <IsTestProject>true</IsTestProject>
</PropertyGroup>

Set GODOT_BIN to a Godot Mono executable:

$env:GODOT_BIN = 'C:\Dev\Godot\4.7.2.stable\Godot_v4.7.2-stable_mono_win64.exe'

Write a test:

namespace GodotProject.Tests;

using System.Threading.Tasks;
using Godot;
using Asteroid.GodotUnitTest;
using NUnit.Framework;

[GodotTestFixture]
public sealed class PlayerTests : GodotSceneTest
{
    [GodotTest]
    public async Task NodeEntersTree()
    {
        var node = AddChild(new Node2D());

        await WaitFrames();

        Assert.That(node.IsInsideTree(), Is.True);
    }
}

Run it through VSTest:

dotnet test

Do not reference NUnit3TestAdapter in the same Godot test project unless you intentionally want NUnit to discover a separate set of tests. Asteroid.GodotUnitTest discovers Godot* attributes and can use NUnit assertions without the NUnit adapter.

The adapter generates AsteroidGodotUnitTestRunner/AsteroidGodotUnitTestRunnerScene.cs inside the Godot project before execution. Add AsteroidGodotUnitTestRunner/ to the consumer project's .gitignore.

Current capabilities

  • dotnet test --list-tests discovery through a VSTest adapter.
  • dotnet test execution through Godot headless by default, with opt-in windowed runs.
  • [GodotTestFixture] classes, [GodotTest] methods, and [GodotTestCase] parameterized cases.
  • Assertions through NUnit Assert or any assertion library that throws exceptions.
  • Failure artifacts: the scene tree dump at the failure moment is folded into the error message, and a PNG screenshot of the failure viewport is written when a frame buffer is available.
  • void and Task test methods.
  • Skip reporting with [GodotTest(Skip = "reason")] or [GodotTestCase(Skip = "reason")].
  • Display names with [GodotTest("Display name")], [GodotTestCase(..., DisplayName = "name")], or DisplayName.
  • GodotSetUp, GodotTearDown, GodotOneTimeSetUp, and GodotOneTimeTearDown lifecycle methods.
  • GodotSceneTest and GodotSceneTestContext helpers for per-test scene roots, node cleanup, frame waits, timers, signals, and scene loading.
  • Per-test Console.Out and Console.Error capture in VSTest/TRX output.
  • Godot file logging disabled by default for adapter-launched test processes, with a runsettings opt-out.
  • Categories with [GodotCategory("name")], surfaced as VSTest TestCategory traits.
  • Run mode selection with [GodotRunMode(GodotRunMode.Windowed)] or .runsettings.
  • Per-test timeout metadata through GodotTestAttribute.TimeoutMilliseconds.
  • Basic NUnit assertion exception interop (Assert.Ignore, Assert.Inconclusive, Assert.Pass).
  • Basic VSTest filtering for FullyQualifiedName, DisplayName, ManagedType, ManagedMethod, RunMode, SkipReason, TimeoutMilliseconds, and TestCategory.

Project layout

  • src/Asteroid.GodotUnitTest/ — lightweight API referenced by Godot test projects.
  • src/Asteroid.GodotUnitTest.TestAdapter/ — VSTest adapter and Godot process runner.
  • godot-project/ — sample Godot C# project used for end-to-end validation.
  • tests/Asteroid.GodotUnitTest.TestAdapter.Fixtures/ — fixture assembly for scanner tests.
  • tests/Asteroid.GodotUnitTest.TestAdapter.Tests/ — adapter unit tests.
  • tests/Asteroid.GodotUnitTest.ExpectedFailureProject/ — Godot project used by E2E tests to verify failure reporting.
  • tests/Asteroid.GodotUnitTest.EndToEnd.Tests/ — optional E2E tests that run dotnet test when GODOT_BIN is configured.

Writing tests

namespace GodotProject.Tests;

using System;
using System.Threading.Tasks;
using Godot;
using Asteroid.GodotUnitTest;
using NUnit.Framework;

[GodotTestFixture]
[GodotCategory("Sample")]
public sealed class PlayerTests
{
    private Node2D node = null!;

    [GodotSetUp]
    public void SetUp()
    {
        node = new Node2D();
    }

    [GodotTearDown]
    public void TearDown()
    {
        node.Free();
    }

    [GodotTest]
    [GodotCategory("Fast")]
    public void CanCreateNode()
    {
        Assert.That(node, Is.Not.Null);
        Assert.That(node, Is.TypeOf<Node2D>());
    }

    [GodotTest("Async example")]
    public async Task AsyncPasses()
    {
        await Task.Delay(1);
        Assert.That(2 + 2, Is.EqualTo(4));
    }

    [GodotTest("Console output example")]
    public void CapturesConsoleOutput()
    {
        Console.WriteLine("This is attached to the VSTest result.");
        Console.Error.WriteLine("stderr is attached under the same test result.");
        Assert.That(node, Is.Not.Null);
    }

    [GodotTest(TimeoutMilliseconds = 1000)]
    public async Task TimeoutSettingPasses()
    {
        await Task.Delay(1);
        Assert.That(true, Is.True);
    }

    [GodotTest("NUnit ignore example")]
    public void NUnitIgnoreIsReportedAsSkipped()
    {
        Assert.Ignore("NUnit Ignore is reported as a skipped Godot test.");
    }

    [GodotTest(Skip = "Waiting on a scene fixture.")]
    public void SkippedTest()
    {
        Assert.Fail("This should not run.");
    }
}

Plain [GodotTest] methods must be parameterless. Use [GodotTestCase] when a test method needs arguments; one VSTest case is discovered for each attribute instance.

[GodotTestCase(1, 2, 3)]
[GodotTestCase(2, 3, 5, DisplayName = "2 + 3 = 5")]
public void AddsNumbers(int left, int right, int expected)
{
    Assert.That(left + right, Is.EqualTo(expected));
}

Methods may be static or instance methods and may return void or Task. [GodotTestFixture] is optional today; methods marked with [GodotTest] or [GodotTestCase] are discovered even without it. TimeoutMilliseconds applies to test methods, test cases, setup methods, and teardown methods; a synchronously blocked method still relies on the adapter/process-level timeout.

Godot scene helpers

Use GodotSceneTest when a fixture needs convenient access to the live Godot SceneTree:

[GodotTestFixture]
public sealed class PlayerSceneTests : GodotSceneTest
{
    [GodotTest]
    public async Task PlayerNodeEntersTree()
    {
        var player = AddChild(new Node2D());

        await WaitFrames();

        Assert.That(player.IsInsideTree(), Is.True);
        Assert.That(player.GetParent(), Is.SameAs(Root));
    }
}

GodotSceneTestContext is also available as a lower-level constructor injection API if you do not want to derive from GodotSceneTest.

Each test gets its own temporary Root node under the runner's SceneTree.Root. Nodes added through AddChild, scenes created through Instantiate/LoadScene, and objects registered with RegisterForCleanup are cleaned up after teardown. Static tests continue to run but cannot use fixture constructor context or instance base-class helpers.

Available helpers:

  • AddChild<T>(T node) adds a node under the per-test root and registers it for cleanup.
  • Instantiate<T>(PackedScene scene) instantiates, parents, and tracks a packed scene instance.
  • LoadScene<T>(string resourcePath) loads a PackedScene resource and instantiates it.
  • FindChild<T>(string name, bool recursive = true, bool owned = false) finds a child node under the per-test root.
  • WaitForChild<T>(string name, double timeoutSeconds = 5, ...) waits until a child node exists or the timeout expires.
  • WaitFrames(int frameCount = 1) waits for one or more process frames.
  • WaitFrames(int frameCount, CancellationToken cancellationToken) waits for frames with cancellation support.
  • WaitSeconds(double seconds) waits using a Godot timer.
  • WaitSeconds(double seconds, CancellationToken cancellationToken) waits using a Godot timer with cancellation support.
  • WaitUntil(Func<bool> condition, double timeoutSeconds = 5, CancellationToken cancellationToken = default) polls once per frame until a condition is true.
  • WaitSignal(GodotObject source, StringName signalName, double timeoutSeconds = 5) waits for a signal with an optional timeout.
  • WaitSignal(..., CancellationToken cancellationToken) overloads add cancellation support.

Lifecycle methods

Use [GodotOneTimeSetUp] and [GodotOneTimeTearDown] for fixture-level lifecycle work. One-time setup runs before the selected tests in that fixture; one-time teardown runs after those selected tests. If one-time setup fails, each selected test in the fixture is reported as failed and one-time teardown is still attempted.

Use [GodotSetUp] and [GodotTearDown] for per-test lifecycle work. Lifecycle methods may be public or non-public, instance or static, and may return void or Task.

Overall execution order is deterministic:

base GodotOneTimeSetUp methods, metadata order
derived GodotOneTimeSetUp methods, metadata order
  base GodotSetUp methods, metadata order
  derived GodotSetUp methods, metadata order
  test method
  derived GodotTearDown methods, reverse metadata order
  base GodotTearDown methods, reverse metadata order
derived GodotOneTimeTearDown methods, reverse metadata order
base GodotOneTimeTearDown methods, reverse metadata order

Lifecycle methods are collected per declared type in the inheritance chain, so private base class setup/teardown methods are supported. Static setup/teardown methods are also supported. Per-test setup/teardown methods run once per test; one-time setup/teardown methods run once per selected fixture group.

If instance creation succeeds and lifecycle execution starts, teardown methods are attempted even when setup or the test body fails. Teardown methods should tolerate partially initialized state. A teardown failure turns a passed or skipped test into a failed test; if the test already failed, the teardown failure is appended to the existing error message and stack trace.

Categories and filtering

Use [GodotCategory] on a class or method to add VSTest TestCategory traits. Class-level categories apply to tests declared in that class. Method-level categories are additive. Duplicate category names are emitted once per test.

[GodotCategory("Scene")]
public sealed class PlayerSceneTests
{
    [GodotTest]
    [GodotCategory("Fast")]
    public void CanCreatePlayer()
    {
    }
}

Filter with standard VSTest filter syntax:

dotnet test --filter "TestCategory=Fast"
dotnet test --filter "TestCategory!=Slow"
dotnet test --filter "TestCategory=Scene&FullyQualifiedName~Player"
dotnet test --filter "RunMode=Windowed&TestCategory=Visual"

Asteroid.GodotUnitTest category filtering does not require NUnit3TestAdapter; the categories are discovered from Godot-prefixed attributes by Asteroid.GodotUnitTest.TestAdapter.

Godot run mode

Tests run in Godot headless mode by default. Use GodotRunMode when a fixture or individual test needs a real Godot window, display driver, or non-headless rendering path.

[GodotRunMode(GodotRunMode.Windowed)]
public sealed class VisualRenderingTests
{
    [GodotTest]
    public void OpensAWindow()
    {
    }

    [GodotTest]
    [GodotRunMode(GodotRunMode.Headless)]
    public void OverridesTheFixtureDefault()
    {
    }
}

GodotRunMode can be applied to a class or method. Method-level run mode overrides class-level run mode. Because headless/windowed mode is a Godot process startup choice, mixed runs are split into separate Godot process invocations.

Filter by effective run mode with standard VSTest syntax:

dotnet test --filter "RunMode=Windowed"

Configuring a Godot project

For local development in this repository, the sample project references the API and adapter projects directly:

<ItemGroup>
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
  <PackageReference Include="NUnit" Version="4.4.0" />
  <ProjectReference Include="..\src\Asteroid.GodotUnitTest\Asteroid.GodotUnitTest.csproj" />
  <ProjectReference Include="..\src\Asteroid.GodotUnitTest.TestAdapter\Asteroid.GodotUnitTest.TestAdapter.csproj" />
</ItemGroup>

It also marks the Godot project as a test project:

<PropertyGroup>
  <IsTestProject>true</IsTestProject>
</PropertyGroup>

When consuming packed NuGet packages from a Godot project, reference the packages instead:

<ItemGroup>
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
  <PackageReference Include="NUnit" Version="4.4.0" />
  <PackageReference Include="Asteroid.GodotUnitTest" Version="0.6.0" />
  <PackageReference Include="Asteroid.GodotUnitTest.TestAdapter" Version="0.6.0" />
</ItemGroup>

Minimal consumer project shape:

<Project Sdk="Godot.NET.Sdk/4.7.2">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
    <PackageReference Include="NUnit" Version="4.4.0" />
    <PackageReference Include="Asteroid.GodotUnitTest" Version="0.6.0" />
    <PackageReference Include="Asteroid.GodotUnitTest.TestAdapter" Version="0.6.0" />
  </ItemGroup>
</Project>

Add the generated runner directory to the consumer project's .gitignore:

AsteroidGodotUnitTestRunner/

The adapter package includes a buildTransitive props file that copies the runner template to the consumer output, so package consumers should not need ProjectReference or TestAdaptersPaths. Do not reference NUnit3TestAdapter in the same Godot test project unless you intentionally want NUnit to discover a separate set of tests; Asteroid.GodotUnitTest discovers Godot* attributes and can use NUnit assertions without the NUnit adapter.

During local project-reference development, godot-project/Directory.Build.props points VSTest at the adapter output directory:

<Project>
  <PropertyGroup>
    <TestAdaptersPaths>$(MSBuildProjectDirectory)\..\src\Asteroid.GodotUnitTest.TestAdapter\bin\$(Configuration)\net8.0</TestAdaptersPaths>
  </PropertyGroup>
</Project>

When this project is consumed as a NuGet package, this local TestAdaptersPaths workaround should no longer be necessary if the adapter package is laid out correctly.

Run settings

Set GODOT_BIN to the Godot Mono executable, either in the environment or in .runsettings.

Example environment variable on Windows:

$env:GODOT_BIN = 'C:\Dev\Godot\4.7.2.stable\Godot_v4.7.2-stable_mono_win64.exe'

Minimal .runsettings:

<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
  <RunConfiguration>
    <ResultsDirectory>./TestResults</ResultsDirectory>
    <TestSessionTimeout>600000</TestSessionTimeout>
  </RunConfiguration>
  <AsteroidGodotUnitTest>
    <ResultTimeoutMilliseconds>600000</ResultTimeoutMilliseconds>
    <RebuildBeforeRun>true</RebuildBeforeRun>
    <DisableGodotFileLogging>true</DisableGodotFileLogging>
  </AsteroidGodotUnitTest>
</RunSettings>

Optional settings:

<AsteroidGodotUnitTest>
  <GodotBin>C:\path\to\Godot_v4.7.2-stable_mono_win64.exe</GodotBin>
  <GodotProjectPath>C:\path\to\project-containing-project.godot</GodotProjectPath>
  <RunMode>Headless</RunMode>
  <Headless>true</Headless>
  <ResultTimeoutMilliseconds>600000</ResultTimeoutMilliseconds>
  <RebuildBeforeRun>true</RebuildBeforeRun>
  <DisableGodotFileLogging>true</DisableGodotFileLogging>
</AsteroidGodotUnitTest>

Runsettings reference:

Setting Default Purpose Notes
GodotBin GODOT_BIN environment variable Path to the Godot Mono executable. May also be supplied as <RunConfiguration><EnvironmentVariables><GODOT_BIN>.
GodotProjectPath unset Explicit path to the directory containing project.godot. When omitted, the adapter walks upward from the test assembly path.
RunMode Headless Default run mode for tests without [GodotRunMode]. Use Windowed for tests that need a real window/display path.
Headless unset Compatibility shortcut for older configuration. false maps to RunMode=Windowed; prefer RunMode.
ResultTimeoutMilliseconds 600000 Process-level timeout for dotnet build and Godot execution. Per-test TimeoutMilliseconds is separate and cannot interrupt synchronously blocked code.
RebuildBeforeRun true Builds the Godot project before launching Godot. Set false only when the project is already built.
DisableGodotFileLogging true Prevents adapter-launched Godot processes from writing normal project log files. Set false to opt out and keep Godot file logs.

If GodotProjectPath is omitted, the adapter walks upward from the discovered test assembly until it finds project.godot.

Set <RunMode>Windowed</RunMode> to make unannotated tests run without --headless. [GodotRunMode(...)] on a class or method overrides the .runsettings default for matching tests. <Headless>false</Headless> is also accepted as a compatibility shortcut for windowed mode.

Godot file logging is disabled by default during test runs. The adapter passes debug/file_logging/enable_file_logging=false, redirects any early Godot log file to a temporary adapter path, and deletes that temporary log after the process exits. The adapter still captures Godot standard output and standard error for VSTest/TRX diagnostics. Set <DisableGodotFileLogging>false</DisableGodotFileLogging> if you need Godot to write its normal project log files.

Running tests

Build and test entry points live in the host repository (AsteroidFrameworkDevelop). Adapter unit tests (no Godot required):

dotnet test tests/Managed/Asteroid.GodotUnitTest.TestAdapter.Tests

The sample Godot project and the end-to-end validation live under tests/Godot/Asteroid.GodotUnitTest.E2E.Tests/ (sample-project TRX reconciliation, an expected-failure project, and a package layout smoke test). They require GODOT_BIN to point at a Godot Mono executable and skip cleanly (Assert.Inconclusive) when it is absent; they are minute-scale and invoked explicitly:

dotnet test tests/Godot/Asteroid.GodotUnitTest.E2E.Tests

Generated runner files (injection contract)

Before execution, the adapter writes a Godot C# runner into the target project under:

AsteroidGodotUnitTestRunner/AsteroidGodotUnitTestRunnerScene.cs

The design rests on two hard constraints: godot -s res://…cs requires a real .cs file inside the project, and the runner must compile against the host's GodotSharp/NUnit versions. Source injection satisfies both; the contract has four parts:

  1. Single source of truth = the in-package template (Runner/AsteroidGodotUnitTestRunnerScene.cs.template). Every test run rewrites the file unconditionally (File.WriteAllText) — runner/adapter version pairing is a machine guarantee, not discipline. This is the mechanism's core value: the runner↔adapter handshake (CLI argument contract, result file format) stays single-sourced in one package.
  2. First line // <auto-generated /> — format/style engines exempt the injected file by convention; its namespace, indentation, and law compliance are therefore established at the template source. Changing the template requires an adapter version bump (same-version source drift is not allowed).
  3. **/AsteroidGodotUnitTestRunner/ is gitignored — the materialized copy is a regeneration artifact with zero git footprint. If a local copy is ever damaged by external tooling, the next test run self-heals it (or regenerate manually from the template).
  4. The injected source enters the consumer's compile face — version co-compilation comes free, at the cost that compile-level gates also apply to it; the template must be kept law-clean at source.

Alternatives were assessed and rejected: an addon-hosted runner splits the runner↔adapter protocol across two distribution axes (submodule + NuGet), and a precompiled runner dll adds a second version coupling axis (host NUnit + GodotSharp). Do not edit the generated file by hand. The older .haze_godot_unit/ directory name is also ignored as a legacy generated location.

Package layout smoke test

The E2E suite (PackagesContainExpectedLayout) packs both projects in Release and asserts the package layout: the API dll, the adapter dll, the packaged runner template under lib/net8.0/Runner/, and the buildTransitive props file.

Compatibility policy

The 0.x compatibility promise covers the user-facing API and configuration surface: GodotTestAttribute, GodotTestCaseAttribute, GodotTestFixtureAttribute, lifecycle attributes, GodotCategoryAttribute, GodotRunModeAttribute, GodotRunModeEnum, GodotSceneTest, GodotSceneTestContext, and the documented runsettings schema.

The generated runner protocol DTOs and adapter internals are public only where the runner needs to share assembly contracts with the adapter. They are not intended for direct user code and may change between 0.x releases.

Before 1.0.0, the project should have successful feedback from at least two real Godot C# consumers, complete runsettings documentation, final public API review, and confirmed package-source policy (currently: official nuget.org).

Packing and release

Both projects carry self-contained package metadata (version, license, repository) and pack independently:

dotnet pack Asteroid.GodotUnitTest/Asteroid.GodotUnitTest.csproj -c Release -o artifacts/packages
dotnet pack Asteroid.GodotUnitTest.TestAdapter/Asteroid.GodotUnitTest.TestAdapter.csproj -c Release -o artifacts/packages

Packages are written to artifacts/packages/ and include symbol packages (.snupkg). Releases go to official nuget.org (dotnet nuget push ... --source https://api.nuget.org/v3/index.json --api-key $NUGET_API_KEY --skip-duplicate); note the Chinese NuGet mirror may lag the official feed by several minutes after a push.

Before pushing a 0.x package:

  1. Ensure package descriptions, README, and CHANGELOG.md describe the release accurately.
  2. Run the host repository test suites: adapter unit tests plus the E2E suite with GODOT_BIN set.
  3. Verify package consumption from a separate Godot C# project and confirm the adapter package exposes its runner template correctly.

Troubleshooting

Failure phase diagnostics

Failed test results may include a Failure phase: ... prefix in the TRX error message. This identifies where the runner was when the failure was recorded:

  • TestDiscovery, TestMetadata, or TestCaseArguments means the runner could not resolve the selected method or parameterized case metadata.
  • OneTimeSetUp, SetUp, Test, TearDown, OneTimeTearDown, Cleanup, or OneTimeCleanup points to the matching lifecycle or cleanup step.
  • SceneContext or FixtureConstruction means the temporary scene root or fixture instance could not be created.
  • Runner means the Godot runner failed before it could produce normal per-test results.

When the Godot process exits unexpectedly, the adapter includes a summarized copy of Godot standard output and standard error in the failure message so the root cause is visible from dotnet test or TRX output.

At normal verbosity and above, execution also reports the effective runsettings summary, selected test count, project root, run mode, and mixed run mode process grouping.

Godot executable not found

Set GODOT_BIN to the Godot Mono executable, or add <GodotBin> under <AsteroidGodotUnitTest> in .runsettings. The adapter reports both the configured and expanded path when the file is missing.

project.godot not found

If GodotProjectPath is omitted, the adapter walks upward from the discovered test assembly until it finds project.godot. If your test assembly is outside the Godot project directory, configure an absolute GodotProjectPath in .runsettings.

Adapter not discovered

Ensure the Godot test project references Microsoft.NET.Test.Sdk and either references Asteroid.GodotUnitTest.TestAdapter as a package or points TestAdaptersPaths at the local adapter output during project-reference development. Package consumers should not need TestAdaptersPaths.

Runner template not found

The adapter first looks for the runner template as an embedded resource, then near the adapter assembly and test output. Package consumers rely on the adapter package's buildTransitive props file to copy AsteroidGodotUnitTestRunnerScene.cs.template into the output directory. Rebuild/restore the consumer project if the copied template is stale or missing.

Stale generated runner

AsteroidGodotUnitTestRunner/AsteroidGodotUnitTestRunnerScene.cs is generated. If template changes are not reflected, delete the generated AsteroidGodotUnitTestRunner/ directory and rebuild before running tests again.

Duplicate NUnit discovery

Do not reference NUnit3TestAdapter in the same Godot test project unless you intentionally want NUnit to discover a separate set of tests. Asteroid.GodotUnitTest discovers Godot* attributes and can use NUnit assertions without the NUnit adapter.

Timeout expectations

TimeoutMilliseconds can fail awaited Task tests and methods that complete after their timeout. It cannot interrupt a synchronously blocked method before control returns to the runner; use ResultTimeoutMilliseconds or VSTest session timeout for process-level protection.

Deliberately out of scope for the current 0.x line

  • Advanced scene/input simulation helpers.
  • Named-pipe or streaming result protocol.
  • Reusing a long-lived Godot process.
  • Running pure .NET tests outside Godot.

Asteroid.GodotUnitTest 是让 Godot C# 工程的测试被 dotnet test 发现与执行的测试基建:发现走 VSTest 标准扩展点,执行体跑在一个由适配器拉起的真 Godot 进程里。本模块不依赖框架任何其他 库(仅 GodotSharp + VSTest ObjectModel + Mono.Cecil),服务对象是"写 Godot 测试的测试工程", 框架运行时与游戏运行时零接触。

双包分工

工程 运行位置 内容
Asteroid.GodotUnitTest Godot 进程内 [GodotTest] 等特性族(纯标记)、GodotSceneTest/GodotSceneTestContext 场景基类与等待 helper、runner 协议 DTO
Asteroid.GodotUnitTest.TestAdapter dotnet test 进程内 GodotUnitDiscoverer(发现)、GodotUnitExecutor(执行)、TestAssemblyScanner(Cecil 扫描)、GodotProcessRunner(进程编排)、GodotUnitSettings(runsettings)

两个工程都面向 net8.0——这是消费者兼容面(VSTest 生态与 Godot .NET 运行时口径),与宿主仓的 net10.0 默认值无关;csproj 显式声明覆盖。

一次 dotnet test 的完整旅程

  1. 发现:GodotUnitDiscoverer 用 Mono.Cecil 只读测试程序集元数据(不加载执行),扫出 Godot* 特性标注的测试,连同类别/运行模式/超时/跳过原因作为 TestCase 属性上报。 只认 Godot* 特性;NUnit 仅作断言库,不与 NUnit3TestAdapter 同引(会双发现)。
  2. 分组与进程编排:GodotUnitExecutor 按有效 GodotRunModeEnum(headless/windowed)把选中 测试分组,每组各起一个 Godot 进程;写 selected-tests.json,起 godot --path <工程> --headless -s res://AsteroidGodotUnitTestRunner/AsteroidGodotUnitTestRunnerScene.cs 并传程序集/清单/结果三个路径参数。runsettings 的 RebuildBeforeRun=true 时先触发构建。
  3. Godot 侧执行:runner 场景是 -s 独立脚本模式(不跑主场景)的 SceneTree 子类,源码由 适配器嵌入资源提供(dll 自带;输出目录复制与包内 lib/net8.0/Runner/ 只是兜底与包形态 兜底),写入 Godot 工程后随其编译。runner 反射加载测试程序集,按继承树收集 [GodotSetUp]/[GodotTearDown](同步异步都收),逐测试执行,把结果序列化为 result.json 后 Quit(exitCode)。
  4. 回传:文件协议,不走 stdout(stdout 只做日志转发)。适配器读回 result.json 报给 VSTest;进程退出码非零且结果文件缺失才判为失败,有文件则照读报告。

裁决记录

  • 分发形态(2026-09-12 终裁决):本模块是零框架依赖的开发期工具(同 GodotCli/TablerIcons 先例),落点 Develop 仓 tooling/Utility/,以 NuGet 包分发(0.6.0 起发 nuget.org 官方源)—— 游戏项目 dotnet add package 两行接入;Develop 仓内消费者保持 ProjectReference 活源。 演进链:曾判"随 Framework 子模块源码分发"→深入消费场景后改判(框架入口带测试依赖被驳: TestSdk 不可传递+生产依赖图污染+条件排除致 release 测试脱钩)。历史 0.5.0 包与 v0.5.0 tag 留在旧 Gitea 仓作锚点,旧仓待整仓归档。
  • 枚举命名:GodotRunModeEnum 遵守仓规 AST1010;特性名 [GodotRunMode] 保持(收编时该库 尚无外部消费者,改名无破坏)。
  • 等待 helper 单签名:WaitFrames/WaitSeconds/WaitSignal 收敛为"可选参数 + 默认 CancellationToken"单签名;WaitSignal 的非正超时表示无限等待(仅受取消支配)——对齐 "API 统一可选参数"教义。
  • 版本矩阵:Godot 4.7.2 Mono / Windows / .NET SDK 10;包目标 net8.0 是消费者兼容面, 非仓默认框架。

测试布局(消费侧)

  • 纯单测(外层 tests/Managed/Asteroid.GodotUnitTest.TestAdapter.Tests):扫描器、过滤器、 进程参数构造、runsettings 解析、API 面。
  • 活体与端到端(外层 tests/Godot/):Fixtures(真 Godot 进程内的特性矩阵活体)、E2E 两场景 (样例工程 trx 对账 / 反例工程故意失败对账)。E2E 依赖 GODOT_BIN 指向 Godot Mono 可执行文件, 缺失时优雅跳过;分钟级,显式调用。
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.7.4 92 9/25/2026
0.7.3 84 9/25/2026
0.7.2 87 9/25/2026
0.7.1 91 9/21/2026
0.7.0 104 9/16/2026
0.6.0 101 9/12/2026