Testen
Valkarn Tasks wird mit einer dedizierten Test-Infrastruktur geliefert, mit der Sie schnelle, deterministische Unit-Tests für asynchronen Code schreiben können — keine echten Timer, keine Frame-Wartezeiten, kein Unity Editor für die Kern-Test-Suite erforderlich.
Überblick
Die Test-Unterstützung befindet sich in der Testing/-Assembly (UnaPartidaMas.Valkarn.Tasks.Testing), die über InternalsVisibleTo für die Laufzeit sichtbar ist. Sie stellt zwei öffentliche Typen bereit:
| Typ | Zweck |
|---|---|
ValkarnTaskTestHelper | Valkarn Tasks-Laufzeit für eine Test-Session initialisieren und beenden |
TestClock | Deterministischer Zeit- und Frame-Anbieter; ersetzt den Unity-TimeProvider während Tests |
ValkarnTaskTestHelper
ValkarnTaskTestHelper ist eine statische Hilfsklasse im Namespace UnaPartidaMas.Valkarn.Tasks.Testing.
Was es tut
Setup() führt drei Dinge durch:
- Erstellt eine
TestClockund installiert sie alsTimeProvider.Current, wobei jeder echte Unity-Zeitanbieter ersetzt wird. - Ruft
PlayerLoopHelper.InitializeForTest()auf, das dieContinuationQueue- undPlayerLoopRunner-Arrays für alle 16 PlayerLoop-Timings alloziert und die Laufzeit als initialisiert markiert. - Gibt die
TestClockzurück, damit Ihr Test die Zeit steuern kann.
Teardown() kehrt das Setup um:
- Installiert eine neue, keine-Operation-
TestClockalsTimeProvider.Current, um zu verhindern, dass veraltete Zeit zwischen Test-Fixtures durchsickert. - Ruft
PlayerLoopHelper.ShutdownForTest()auf, das die Warteschlangen und Runner abbaut.
Verwendungsmuster
using NUnit.Framework;
using UnaPartidaMas.Valkarn.Tasks;
using UnaPartidaMas.Valkarn.Tasks.Testing;
[TestFixture]
public class MyAsyncTests
{
TestClock clock;
[SetUp]
public void SetUp()
{
clock = ValkarnTaskTestHelper.Setup();
}
[TearDown]
public void TearDown()
{
ValkarnTaskTestHelper.Teardown();
}
// Tests kommen hierher
}
Rufen Sie Setup einmal pro Test in [SetUp] und Teardown einmal pro Test in [TearDown] auf. Das Muster stellt sicher, dass jeder Test mit einem sauberen Laufzeitzustand beginnt und nicht in den nächsten Test durchsickert.
Standard-Delta-Zeit
Setup akzeptiert einen optionalen defaultDeltaTime-Parameter (Standard: 1/60 Sekunden, d. h. 60 fps):
clock = ValkarnTaskTestHelper.Setup(defaultDeltaTime: 1f / 30f); // 30-fps-Simulation
Dieser Wert wird von TestClock.AdvanceFrame() und TestClock.AdvanceFrames(n) verwendet.
TestClock
TestClock implementiert ITimeProvider und gibt Tests volle Kontrolle über:
- Simulierte Zeit (via
GetTimestamp(), durchStopwatch.Frequency-Ticks unterstützt) DeltaTimeundUnscaledDeltaTimefür den aktuellen FrameFrameCount
Zeit voranbringen
Advance(TimeSpan duration)
Bringt die Zeit um die angegebene Dauer in einem einzigen Schritt voran. Die gesamte Dauer wird als DeltaTime für diesen Frame angewendet, FrameCount wird um 1 erhöht und alle PlayerLoop-Timings werden verarbeitet. Nach der Verarbeitung wird DeltaTime auf den Wert zurückgesetzt, den es vor dem Aufruf hatte.
var task = ValkarnTask.Delay(3000); // 3-Sekunden-Verzögerung
clock.Advance(TimeSpan.FromSeconds(2));
Assert.IsFalse(task.IsCompleted); // noch nicht
clock.Advance(TimeSpan.FromSeconds(1));
Assert.IsTrue(task.IsCompleted); // genau bei 3 Sekunden
Verwenden Sie dies, wenn Sie zu einem bestimmten Zeitpunkt springen möchten, ohne einzelne Frames zu simulieren.
AdvanceFrame()
Bringt einen Frame mit der aktuellen DeltaTime voran. FrameCount wird um 1 erhöht, alle PlayerLoop-Timings werden verarbeitet, und ein 1-ms-Thread.Sleep wird vor der Verarbeitung eingefügt. Der Sleep existiert, weil Hintergrund-Worker-Threads (verwendet von RunOnThreadPool und Unity Job System-Integration) ein kleines Zeitfenster benötigen, um zwischen Frame-Ticks abzuschließen — ohne ihn laufen Tests Frames ohne Lücke hintereinander, was Race Conditions verursacht, die in echten Unity-Frames nicht auftreten.
clock.AdvanceFrame();
AdvanceFrames(int count)
Ruft AdvanceFrame() count Mal auf.
clock.AdvanceFrames(10); // 10 Frames simulieren
ProcessTick(PlayerLoopTiming timing)
Verarbeitet eine einzelne PlayerLoop-Timing-Phase ohne Zeitfortschritt oder FrameCount-Erhöhung. Nützlich beim Testen von Code, der für ein bestimmtes Timing geplant ist (z. B. PlayerLoopTiming.FixedUpdate), und Sie nicht alle anderen Timings verarbeiten möchten.
clock.ProcessTick(PlayerLoopTiming.FixedUpdate);
Delta-Zeit steuern
SetDeltaTime(float deltaTime)
Setzt sowohl DeltaTime als auch UnscaledDeltaTime auf denselben Wert für alle nachfolgenden Frames.
SetDeltaTime(float deltaTime, float unscaledDeltaTime)
Setzt sie unabhängig voneinander, um eine Nicht-Einheits-Time.timeScale zu simulieren. Zum Beispiel, um Code zu testen, der DelayType.UnscaledDeltaTime verwendet, während die Zeit pausiert ist:
clock.SetDeltaTime(deltaTime: 0f, unscaledDeltaTime: 0.05f);
var scaledTask = ValkarnTask.Delay(500, DelayType.DeltaTime);
var unscaledTask = ValkarnTask.Delay(500, DelayType.UnscaledDeltaTime);
clock.AdvanceFrames(9); // 450 ms unskaliert, 0 ms skaliert
Assert.IsFalse(scaledTask.IsCompleted); // pausierte Spielzeit erreicht niemals 500ms
Assert.IsFalse(unscaledTask.IsCompleted); // 450ms < 500ms
clock.AdvanceFrame(); // 500 ms unskaliert
Assert.IsTrue(unscaledTask.IsCompleted); // unskalierte Verzögerung fertig
Assert.IsFalse(scaledTask.IsCompleted); // skaliert bewegt sich immer noch nie
Unit-Tests für async ValkarnTask-Methoden schreiben
Tests, die synchron abschließen
Einige ValkarnTask-Operationen schließen ohne Suspendierung ab — zum Beispiel das Abwarten von ValkarnTask.CompletedTask, ValkarnTask.FromResult(value) oder einer ValkarnTaskCompletionSource, die vor dem Abwarten abgeschlossen wurde. Diese benötigen überhaupt keine Uhr und können die interne TestHelper-Klasse direkt verwenden (intern für die Test-Assembly):
[TestFixture]
public class SyncTests
{
[SetUp]
public void SetUp()
{
// Setzt nur die Haupt-Thread-ID — keine Uhr oder PlayerLoop benötigt
TestHelper.EnsureInitialized();
}
[Test]
public void FromResult_ReturnsValue()
{
var task = ValkarnTask.FromResult(42);
Assert.IsTrue(task.IsCompleted);
Assert.AreEqual(42, task.GetAwaiter().GetResult());
}
}
TestHelper (in Tests/Editor/TestHelper.cs) ist ein interner Helfer, der von der Test-Suite selbst verwendet wird:
TestHelper.EnsureInitialized()setztValkarnTaskPoolShared.MainThreadIdauf den aktuellen Thread. Dies ist erforderlich, damit Pool-Operationen zum korrekten (nicht-atomaren) Schnellpfad geleitet werden.TestHelper.RunSync(ValkarnTask task)stellt sicher, dass die Task bereits abgeschlossen ist und ruftGetResult()auf — nützlich zum Testen von Code-Pfaden, die synchron abschließen sollen.TestHelper.RunSync<T>(ValkarnTask<T> task)tut dasselbe und gibt den Ergebniswert zurück.TestHelper.UnobservedExceptionCollectorabonniertValkarnTask.UnobservedExceptionfür die Dauer seines Geltungsbereichs und sammelt nicht beobachtete Ausnahmen zur Prüfung.
Tests, die Zeit erfordern
Jeder Test, der ValkarnTask.Delay, ValkarnTask.Yield oder bei einem PlayerLoop-Timing geplanten Code enthält, erfordert ValkarnTaskTestHelper.Setup() und die zurückgegebene TestClock.
Beispiel: Test einer verzögerungsbasierten Methode
Angenommen, Sie haben:
public static async ValkarnTask WaitAndLog(int ms)
{
await ValkarnTask.Delay(ms);
Log("done");
}
Testen Sie es ohne echtes Warten:
[TestFixture]
public class DelayTests
{
TestClock clock;
[SetUp]
public void SetUp() => clock = ValkarnTaskTestHelper.Setup();
[TearDown]
public void TearDown() => ValkarnTaskTestHelper.Teardown();
[Test]
public void WaitAndLog_CompletesAfterDelay()
{
var task = WaitAndLog(3000);
Assert.IsFalse(task.IsCompleted);
clock.Advance(TimeSpan.FromSeconds(2));
Assert.IsFalse(task.IsCompleted);
clock.Advance(TimeSpan.FromSeconds(1));
Assert.IsTrue(task.IsCompleted);
// Ergebnis abrufen (wirft, wenn fehlgeschlagen)
task.GetAwaiter().GetResult();
}
[Test]
public void WaitAndLog_ZeroDelay_CompletesImmediately()
{
var task = WaitAndLog(0);
// ValkarnTask.Delay(0) gibt sofort CompletedTask zurück
Assert.IsTrue(task.IsCompleted);
}
}
Abbruch testen
[Test]
public void Delay_CancelledMidway_ThrowsOCE()
{
var cts = new CancellationTokenSource();
var task = ValkarnTask.Delay(5000, cts.Token);
Assert.IsFalse(task.IsCompleted);
cts.Cancel();
// Abbruch propagiert beim nächsten Tick
clock.AdvanceFrame();
Assert.IsTrue(task.IsCompleted);
Assert.Throws<OperationCanceledException>(() => task.GetAwaiter().GetResult());
}
Nicht beobachtete Ausnahmen sammeln
[Test]
public void FaultedTask_PublishesUnobservedException()
{
using var collector = new TestHelper.UnobservedExceptionCollector();
var tcs = new ValkarnTaskCompletionSource();
var task = tcs.Task;
// Nicht abwarten — fire and forget
task.Forget();
// Mit Ausnahme abschließen — löst nicht-beobachteten Pfad aus
tcs.TrySetException(new InvalidOperationException("oops"));
Assert.AreEqual(1, collector.Exceptions.Count);
Assert.IsInstanceOf<InvalidOperationException>(collector.Exceptions[0]);
}
Beispiel: Test einer Channel-Produzenten/Konsumenten-Pipeline
[TestFixture]
public class ChannelPipelineTests
{
[SetUp]
public void SetUp() => TestHelper.EnsureInitialized();
[Test]
public void Producer_WritesItems_ConsumerReadsInOrder()
{
var channel = ValkarnTask.Channel.CreateUnbounded<int>();
// Produzent: synchron schreiben
channel.Writer.TryWrite(1);
channel.Writer.TryWrite(2);
channel.Writer.TryWrite(3);
channel.Writer.Complete();
// Konsument: ReadAsync auf einem Channel mit Elementen schließt synchron ab
Assert.AreEqual(1, TestHelper.RunSync(channel.Reader.ReadAsync()));
Assert.AreEqual(2, TestHelper.RunSync(channel.Reader.ReadAsync()));
Assert.AreEqual(3, TestHelper.RunSync(channel.Reader.ReadAsync()));
}
[Test]
public void ReadAsync_BeforeWrite_PendsThenCompletesOnWrite()
{
var channel = ValkarnTask.Channel.CreateUnbounded<string>();
// Lesen vor dem Schreiben
var readTask = channel.Reader.ReadAsync();
Assert.IsFalse(readTask.IsCompleted);
// Schreiben schließt das ausstehende Lesen synchron ab
channel.Writer.TryWrite("hello");
Assert.IsTrue(readTask.IsCompleted);
Assert.AreEqual("hello", readTask.GetAwaiter().GetResult());
}
[Test]
public void Complete_DrainedChannel_CompletionTaskCompletes()
{
var channel = ValkarnTask.Channel.CreateUnbounded<int>();
channel.Writer.TryWrite(42);
var completion = channel.Reader.Completion;
Assert.IsFalse(completion.IsCompleted);
channel.Writer.Complete();
Assert.IsFalse(completion.IsCompleted); // Element noch ungelesen
channel.Reader.TryRead(out _);
Assert.IsTrue(completion.IsCompleted); // geleert — Completion wird ausgelöst
}
[Test]
public void ClosedChannel_Read_ThrowsChannelClosedException()
{
var channel = ValkarnTask.Channel.CreateUnbounded<int>();
channel.Writer.Complete();
var readTask = channel.Reader.ReadAsync();
Assert.IsTrue(readTask.IsCompleted);
Assert.Throws<ChannelClosedException>(() => readTask.GetAwaiter().GetResult());
}
}
Beispiel: Test von ValkarnTask.Delay mit Frame-Fortschritt
Dieses Beispiel demonstriert den Test einer Methode, die eine konfigurierbare Verzögerung wartet, und überprüft, dass ein teilweiser Fortschritt die Task nicht vorzeitig abschließt.
[TestFixture]
public class DelayFrameTests
{
TestClock clock;
[SetUp]
public void SetUp() => clock = ValkarnTaskTestHelper.Setup(defaultDeltaTime: 1f / 60f);
[TearDown]
public void TearDown() => ValkarnTaskTestHelper.Teardown();
[Test]
public void Delay_500ms_RequiresTenFramesAt50ms()
{
clock.SetDeltaTime(0.05f); // 50 ms pro Frame
var task = ValkarnTask.Delay(500);
clock.AdvanceFrames(9);
Assert.IsFalse(task.IsCompleted, "450 ms vergangen — sollte noch nicht fertig sein");
clock.AdvanceFrame();
Assert.IsTrue(task.IsCompleted, "500 ms vergangen — sollte fertig sein");
}
[Test]
public void Delay_Realtime_UsesTimestampNotDeltaTime()
{
// Realtime-Verzögerung ignoriert DeltaTime; sie verwendet den Stopwatch-Zeitstempel
clock.SetDeltaTime(0f); // DeltaTime eingefroren
var task = ValkarnTask.Delay(200, DelayType.Realtime);
// Advance bewegt nur den Zeitstempel via TimeSpan
clock.Advance(TimeSpan.FromMilliseconds(199));
Assert.IsFalse(task.IsCompleted);
clock.Advance(TimeSpan.FromMilliseconds(1));
Assert.IsTrue(task.IsCompleted);
}
}
NUnit-Integration
Die Test-Suite verwendet NUnit 3.x (auf 3.x in TestRunner.csproj festgelegt). NUnit 4 hat die klassische Assert.IsTrue / Assert.AreEqual-API entfernt, die die Tests verwenden; aktualisieren Sie nicht auf NUnit 4, ohne die Assertions auf die Constraint-basierte API zu aktualisieren.
Unity Test Framework
Im Unity Editor werden Tests in Tests/Editor/ vom Unity Test Framework (UTF) entdeckt und ausgeführt, das auf NUnit basiert. Die Test-Runner-Integration funktioniert wie folgt:
- UTF führt jeden
[Test]auf dem Haupt-Thread aus. ValkarnTaskTestHelper.Setup()installiert dieTestClockvor jedem Test; UFTs[SetUp]-Attribut ruft es auf.ValkarnTaskTestHelper.Teardown()entfernt die Uhr in[TearDown].- Keine Coroutine oder
[UnityTest]ist erforderlich: alle Valkarn Tasks-Tests verwenden synchrone NUnit-[Test]-Methoden. DieTestClocktreibt die Zeit manuell voran, sodass kein echter Frame-Fortschritt benötigt wird.
Das _TestRunner~-Projekt
Das _TestRunner~/-Verzeichnis enthält ein eigenständiges .NET 8-Projekt, das die gesamte Test-Suite außerhalb von Unity kompiliert und ausführt, mit dem .NET SDK und dotnet test. Dies wird für CI und die lokale Entwicklung von Mitwirkenden verwendet.
Struktur
_TestRunner~/
TestRunner.csproj — .NET 8-Projektdatei
bin~/ — Build-Ausgabe (gitignored)
obj~/ — Zwischenausgabe (gitignored)
Wie es funktioniert
TestRunner.csproj kompiliert vier Quellsätze in eine einzige Assembly:
| Quellsatz | Pfad | Hinweise |
|---|---|---|
| Runtime | ../Runtime/**/*.cs | Alle Runtime-Dateien |
| Testing | ../Testing/**/*.cs | ValkarnTaskTestHelper, TestClock |
| Tests | ../Tests/Editor/**/*.cs | Alle .cs-Testdateien |
| Ausschlüsse | UnityTimeProvider.cs, Bridge/, Burst/, ECS/ | Erfordert echte Unity-APIs — ausgeschlossen |
Das Projekt definiert nicht UNITY_5_3_OR_NEWER, VTASKS_HAS_BURST, VTASKS_HAS_COLLECTIONS oder VTASKS_HAS_ENTITIES, sodass alle Unity-spezifischen #if-Zweige herausgekompiliert werden. Das bedeutet, die Tests üben die reinen C#-Code-Pfade aus.
Die Analyzer-DLLs werden als <Analyzer>-Einträge geladen, sodass dieselben Regeln, die in der IDE ausgelöst werden, auch während dotnet build des Testprojekts ausgelöst werden.
Tests ausführen
cd _TestRunner~
dotnet test
Oder vom Repository-Stamm:
dotnet test _TestRunner~/TestRunner.csproj
Ausgeschlossene Test-Dateien
Drei Test-Unterverzeichnisse sind vom _TestRunner~-Projekt ausgeschlossen, weil sie echte Unity-Laufzeit-APIs erfordern:
| Verzeichnis | Grund |
|---|---|
Tests/Editor/Bridge/ | Tests für die Job System-Bridge; erfordert Unity.Jobs |
Tests/Editor/Burst/ | Tests für den Burst-Scheduler; erfordert Unity.Burst |
Tests/Editor/ECS/ | Tests für ECS-Utilities; erfordert Unity.Entities |
Diese Tests laufen nur innerhalb des Unity Editors über das Unity Test Framework.
Einen neuen Test hinzufügen
- Fügen Sie eine neue
.cs-Datei unterTests/Editor/hinzu (nicht in einem Unity-API-Unterverzeichnis). - Dekorieren Sie die Klasse mit
[TestFixture]und die Methoden mit[Test]. - Wenn der Test Zeitsteuerung benötigt, verwenden Sie
ValkarnTaskTestHelper.Setup()/Teardown()in[SetUp]/[TearDown]. - Wenn der Test nur synchrone Task-Assertions benötigt (keine Verzögerung), verwenden Sie
TestHelper.EnsureInitialized()in[SetUp]— kein Teardown erforderlich. - Führen Sie
dotnet test _TestRunner~/TestRunner.csprojaus, um außerhalb von Unity zu überprüfen. - Führen Sie das Unity Test Framework aus, um innerhalb von Unity zu überprüfen.