Start Debugging

Fix: Der Visual Studio Test-Explorer hängt bei einem xUnit v3 Projekt, während dotnet test durchläuft

Der Test-Explorer steuert xUnit v3 über den Servermodus von Microsoft.Testing.Platform per JSON-RPC-Socket, dotnet test nicht. Setzen Sie UseMicrosoftTestingPlatformRunner, damit beide Pfade derselbe Code sind, oder DisableTestingPlatformServerCapability für den Rückfall auf VSTest.

Der Test-Explorer und dotnet test führen nicht denselben Code aus. Ein xUnit v3 Testprojekt kompiliert eine ausführbare Datei, deren generierter Einstiegspunkt sich anhand eines einzigen Arguments verzweigt: Übergibt Visual Studio --server, gibt der Prozess die Kontrolle an Microsoft.Testing.Platform ab und versucht, einen JSON-RPC-Socket zurück zur IDE zu öffnen; andernfalls läuft der eigene In-Process-Konsolen-Runner von xUnit. Ein grüner Lauf auf der Kommandozeile sagt nichts über den Servermodus-Pfad aus. Der schnellste Fix macht beide Pfade identisch: <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>. Wenn der Test-Explorer sofort funktionieren muss, ergänzen Sie <DisableTestingPlatformServerCapability>true</DisableTestingPlatformServerCapability> und starten Visual Studio neu, um auf den VSTest-Adapter zurückzufallen.

Alles Folgende wurde auf .NET SDK 10.0.201 (Laufzeit 10.0.5) mit xunit.v3 3.2.2, xunit.runner.visualstudio 3.1.5, Microsoft.NET.Test.Sdk 17.14.1 und Microsoft.Testing.Platform 1.9.1 gemessen. Der Mechanismus bleibt in den .NET 11 Vorschauversionen unverändert, weil er in den MSBuild-Targets von xUnit und MTP liegt und nicht im SDK.

Die Symptome, um die es hier geht

Unable to connect to testing platform runner process [MyProject.Tests.dll]

Oder gar kein Fehler: Der Fortschrittsindikator im Test-Explorer dreht sich, die Testanzahl bleibt bei null oder friert mitten in einem Lauf über die gesamte Projektmappe ein, und ein Prozess MyProject.Tests.exe liegt untätig im Task-Manager, bis Sie den Lauf abbrechen. Gleichzeitig:

> dotnet test
Test run summary: Passed!
  total: 2
  failed: 0
  succeeded: 2

Warum Test-Explorer und dotnet test nicht denselben Code ausführen

xUnit v3 Testprojekte sind ausführbare Dateien (<OutputType>Exe</OutputType>), und xunit.v3.msbuildtasks generiert deren Main nach obj/Debug/net10.0/XunitAutoGeneratedEntryPoint.cs. Öffnen Sie diese Datei in einem beliebigen v3-Projekt: Das gesamte Problem steht in einer Zeile.

// Generated by xunit.v3.msbuildtasks 3.2.2, .NET SDK 10.0.201.
// obj/Debug/net10.0/XunitAutoGeneratedEntryPoint.cs, reformatted for width.
public static int Main(string[] args)
{
    if (Enumerable.Any(args, arg => arg == "--server" || arg == "--internal-msbuild-node"))
        return TestPlatformTestFramework
            .RunAsync(args, SelfRegisteredExtensions.AddSelfRegisteredExtensions)
            .GetAwaiter().GetResult();
    else
        return ConsoleRunner.Run(args).GetAwaiter().GetResult();
}

Zwei Runner, eine Binärdatei. dotnet test, dotnet run und ein Doppelklick auf die Exe nehmen den else-Zweig und nutzen den In-Process-Konsolen-Runner von xUnit. Visual Studio nimmt den if-Zweig.

Was Visual Studio übergibt, sieht so aus, und Sie können es von Hand ausführen:

# .NET SDK 10.0.201, xunit.v3 3.2.2. Port number is chosen by the IDE.
> MyProject.Tests.exe --server jsonrpc --client-host 127.0.0.1 --client-port 59999
xUnit.net v3 Microsoft.Testing.Platform v1 Runner v3.2.2+728c1dce01 (64-bit .NET 10.0.5)

Connecting to client host '127.0.0.1' port '59999'
[ServerTestHost.OnCurrentDomainUnhandledException] System.Net.Sockets.SocketException (10061):
No connection could be made because the target machine actively refused it.
   at Microsoft.Testing.Platform.ServerMode.ServerModeManager.MessageHandlerFactory
      .CreateMessageHandlerAsync(CancellationToken cancellationToken)
   at Microsoft.Testing.Platform.Hosts.ServerTestHost.InternalRunAsync()

Die Richtung ist entscheidend. Visual Studio lauscht auf einem Loopback-TCP-Port, und der Testprozess wählt sich dort ein. Ermittlung und Ausführung sind danach JSON-RPC-Anfragen wie testing/discoverTests und testing/runTests über diesen Socket. Alles, was den Prozess daran hindert, diesen Handshake abzuschließen oder danach zu antworten, erscheint als Hänger und nie als fehlgeschlagener Test, weil kein Test je gelaufen ist.

Visual Studio versucht das überhaupt nur, wenn das Projekt die Fähigkeit ankündigt. Sie stammt aus Microsoft.Testing.Platform.targets im Paket microsoft.testing.platform:

<!-- microsoft.testing.platform 2.3.3, buildMultiTargeting/Microsoft.Testing.Platform.targets -->
<ItemGroup Condition=" '$(DisableTestingPlatformServerCapability)' != 'true'
                       AND '$(IsTestingPlatformApplication)' == 'true' ">
  <ProjectCapability Include="TestingPlatformServer" />
  <ProjectCapability Include="TestContainer" />
</ItemGroup>

TestingPlatformServer ist der Schalter. Aktuelle Visual Studio Versionen liefern die Microsoft.Testing.Platform Test-Explorer-Erfahrung standardmäßig aktiviert aus, also wird jedes Projekt mit dieser Fähigkeit über den Servermodus gesteuert, ob gewollt oder nicht.

Die minimale Reproduktion

Ein gewöhnliches xUnit v3 Projekt, nichts Exotisches:

<!-- .NET SDK 10.0.201. Reproduces on net10.0 and net11.0. -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <OutputType>Exe</OutputType>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="xunit.v3" Version="3.2.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
  </ItemGroup>
</Project>

Was dieses Projekt tatsächlich deklariert, lässt sich mit einer Zeile prüfen, und das ist der erste Befehl, wenn sich der Test-Explorer merkwürdig verhält:

# .NET SDK 10.0.201
> dotnet msbuild -getItem:ProjectCapability

Beim obigen Projekt erhalten Sie sowohl TestingPlatformServer als auch TestContainer. Das ist ein Projekt, das für den MTP-Servermodus verdrahtet ist und gleichzeitig den kompletten VSTest-Stack mitschleppt: Der Ausgabeordner enthält Microsoft.Testing.Platform.dll neben Microsoft.VisualStudio.TestPlatform.Common.dll, testhost.exe und xunit.runner.visualstudio.testadapter.dll. Zwei Testplattformen in einem bin-Ordner sind zulässig, bedeuten aber, dass der Runner, den Sie über die Kommandozeile ausführen, nicht zwingend der ist, den die IDE wählt.

Fix 1: beide Codepfade identisch machen, der eigentliche Fix

Eine Eigenschaft setzen, und der generierte Einstiegspunkt kehrt sich um:

<!-- Directory.Build.props, .NET SDK 10.0.201, xunit.v3 3.2.2 -->
<Project>
  <PropertyGroup>
    <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
  </PropertyGroup>
</Project>

Neu kompilieren und XunitAutoGeneratedEntryPoint.cs erneut lesen:

// Generated with UseMicrosoftTestingPlatformRunner=true. The branch is reversed.
public static int Main(string[] args)
{
    if (Enumerable.Any(args, arg => arg == "-automated" || arg == "@@"))
        return ConsoleRunner.Run(args).GetAwaiter().GetResult();
    else
        return TestPlatformTestFramework
            .RunAsync(args, SelfRegisteredExtensions.AddSelfRegisteredExtensions)
            .GetAwaiter().GetResult();
}

Microsoft.Testing.Platform ist jetzt der Standardpfad für jeden Aufruf, ein bestandener lokaler Lauf prüft also wirklich den Code, den der Test-Explorer verwendet. Sie erhalten damit auch die MTP-Kommandozeile, und dort liegen die Diagnosen. --diagnostic ist eine MTP-Option und keine von xUnit; vor dieser Änderung lehnt die Exe sie mit error: unknown option: --diagnostic ab:

# .NET SDK 10.0.201, with UseMicrosoftTestingPlatformRunner=true
> MyProject.Tests.exe --diagnostic
Test run summary: Passed!
  total: 2
> ls bin/Debug/net10.0/TestResults/
log_260813111551948.diag

Diese .diag-Datei ist die Grundlage, wenn der Servermodus stockt. Sie protokolliert die Startsequenz der Plattform und jede JSON-RPC-Nachricht, sodass erkennbar wird, ob der Prozess nie verbunden hat, verbunden hat und nie testing/runTests empfing, oder es empfing und innerhalb eines Tests blockierte.

Kombinieren Sie das mit dem MTP-Modus für dotnet test in der global.json, damit die Kommandozeile nicht mehr über die VSTest-Brücke läuft:

{
  "test": { "runner": "Microsoft.Testing.Platform" }
}

Im MTP-Modus brauchen Sie TestingPlatformDotnetTestSupport nicht mehr, und MTP-Argumente benötigen kein zusätzliches -- mehr.

Fix 2: die Fähigkeit abschalten und auf VSTest zurückfallen

Wenn der Test-Explorer sofort laufen muss, entfernen Sie stattdessen die Fähigkeit:

<!-- Directory.Build.props. Restart Visual Studio after changing this. -->
<PropertyGroup>
  <DisableTestingPlatformServerCapability>true</DisableTestingPlatformServerCapability>
</PropertyGroup>

Das ist eine von xUnit dokumentierte Behelfslösung und tut genau das, was in den Targets steht: TestingPlatformServer verschwindet aus der Fähigkeitsliste, TestContainer bleibt erhalten, weil Microsoft.NET.Test.Sdk sie ebenfalls beisteuert, und Visual Studio ermittelt Tests wieder über xunit.runner.visualstudio. Prüfen statt annehmen:

> dotnet msbuild -p:DisableTestingPlatformServerCapability=true -getItem:ProjectCapability

Der Neustart ist nicht optional. Projektfähigkeiten werden beim Laden des Projekts gelesen, ein reines Neukompilieren belässt die laufende IDE also bei der alten Entscheidung.

Fix 3: die selbst geschriebene Main löschen

Diese Ursache prüft man am besten vor allen anderen, weil sie exakt das gemeldete Muster erzeugt: Kommandozeile grün, Test-Explorer tot. Eine Program.cs mit Top-Level-Anweisungen in einem xUnit v3 Projekt ersetzt nicht einen Teil des generierten Einstiegspunkts, sondern den gesamten, und der Compiler warnt nur:

warning CS7022: The entry point of the program is global code;
ignoring 'XunitAutoGeneratedEntryPoint.Main(string[])' entry point.

Ein Build mit dieser Warnung ist trotzdem erfolgreich. Hier ist eine Program.cs, die vollkommen vernünftig aussieht und den Konsolen-Runner von xUnit von Hand verdrahtet:

// .NET SDK 10.0.201, xunit.v3 3.2.2. This builds, and it breaks Test Explorer.
using Xunit.Runner.InProc.SystemConsole;

return await ConsoleRunner.Run(args);

Ohne Argumente ausgeführt bestehen zwei Tests. So ausgeführt, wie Visual Studio es tut, fehlt die --server-Behandlung schlicht:

> MyProject.Tests.exe
=== TEST EXECUTION SUMMARY ===
   MyProject.Tests  Total: 2, Errors: 0, Failed: 0, Skipped: 0

> MyProject.Tests.exe --server jsonrpc --client-host 127.0.0.1 --client-port 59999
error: unknown option: --server

Der Prozess schreibt auf stdout und beendet sich mit Code 3, ohne den Socket je zu öffnen. Aus Sicht von Visual Studio ist das ein Runner, der sich mit nichts verbunden hat, was als Verbindungsfehler oder als endloses Warten sichtbar wird. Wenn Sie wirklich einen eigenen Einstiegspunkt brauchen, setzen Sie <GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint>, bauen gegen TestApplication.CreateBuilderAsync(args) aus Microsoft.Testing.Platform und reichen args unverändert weiter. Das Verschlucken des Argument-Arrays ist der Fehler.

Fix 4: pro Projektmappe nur eine Hauptversion von Microsoft.Testing.Platform

xUnit veröffentlicht Varianten-Pakete, die unterschiedliche MTP-Hauptversionen festlegen, und deren Mischung innerhalb einer Projektmappe erzeugt das Muster “manche Projekte laufen, andere zufällig nicht” bei einem Lauf über die gesamte Projektmappe. Gemessene Auflösungen bei xunit.v3 3.2.2:

PaketverweisAufgelöstes Microsoft.Testing.Platform
xunit.v31.9.1
xunit.v3.mtp-v22.0.2
xunit.v3.mtp-offkeines, nur VSTest

Wählen Sie eine Variante und setzen Sie sie einmalig in Directory.Build.props statt pro Projekt. Microsofts eigene Hinweise zu TestingPlatformDotnetTestSupport sagen dasselbe für eine andere Eigenschaft: Eine Projektmappe, in der einige Projekte die eine und andere die andere Plattform verwenden, “funktioniert möglicherweise nicht korrekt und ist ein nicht unterstütztes Szenario”. Ein neues Testprojekt aus einer Vorlage ist der übliche Weg, auf dem sich eine Abweichung einschleicht.

Warum ein grünes dotnet test weniger beweist als gedacht

Es gibt eine schlimmere Variante: dotnet test kann mit Code 0 enden, ohne irgendetwas ausgeführt zu haben. Nehmen Sie ein reines MTP-Projekt, ohne Microsoft.NET.Test.Sdk, ohne xunit.runner.visualstudio und ohne test-Abschnitt in der global.json. dotnet test nutzt standardmäßig den VSTest-Modus, findet keinen VSTest-Adapter und meldet Erfolg:

# .NET SDK 10.0.201, xunit.v3 3.2.2 only, no global.json test runner section
> dotnet test
  Determining projects to restore...
  All projects are up-to-date for restore.
> echo $LASTEXITCODE
0

Zwei Tests existieren. Null liefen. Exitcode 0. Ergänzen Sie den Runner-Abschnitt der global.json aus Fix 1, und derselbe Befehl meldet total: 2, succeeded: 2. Wenn Ihre CI derzeit bei einem xUnit v3 Projekt grün ist, prüfen Sie die Testanzahl im Log, bevor Sie ihr vertrauen. --minimum-expected-tests über TestingPlatformCommandLineArguments ist die billige Absicherung:

<PropertyGroup>
  <TestingPlatformCommandLineArguments>--minimum-expected-tests 1</TestingPlatformCommandLineArguments>
</PropertyGroup>

Ähnlich aussehende Fälle, die nicht dieser Fehler sind

Nicht jeder stehengebliebene Test-Explorer-Lauf ist Servermodus. Diese Punkte kommen nach den vier obigen Fixes:

Verwandt

Falls Sie noch entscheiden, auf welches Framework Sie sich festlegen, deckt der gemessene Vergleich von xUnit v3, NUnit und MSTest dieselbe Paketierungstrennung zwischen MTP und VSTest aus dem Blickwinkel der Frameworkwahl ab. Sobald die Suite zuverlässig läuft, verwandelt die GitHub Actions Berichterstattung in MTP 2.3 Fehlschläge in Annotationen im Diff des Pull Requests. Für die Integrationstestebene gibt es eine Anleitung zu WebApplicationFactory in ASP.NET Core 11 und einen Vergleich von WebApplicationFactory mit Testcontainers. Wenn Ihre CI aus einem anderen Grund rund um das Testwerkzeug brach, ist VSTest ohne Newtonsoft.Json die andere Änderung, die dieses Jahr viele traf.

Quellen

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Zurück