Fix: Visual Studio Test Explorer hangs on an xUnit v3 project while dotnet test passes
Test Explorer drives xUnit v3 through Microsoft.Testing.Platform server mode over a JSON-RPC socket, dotnet test does not. Set UseMicrosoftTestingPlatformRunner so both paths are the same code, or set DisableTestingPlatformServerCapability to fall back to VSTest.
Test Explorer and dotnet test are not running the same code. An xUnit v3 test project builds an executable whose generated entry point branches on a single argument: if Visual Studio passes --server, the process hands control to Microsoft.Testing.Platform and tries to open a JSON-RPC socket back to the IDE; otherwise it runs xUnit’s own in-process console runner. A green CLI run tells you nothing about the server-mode path. The fastest fix is to make both paths identical by adding <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>. If you need Test Explorer working right now, add <DisableTestingPlatformServerCapability>true</DisableTestingPlatformServerCapability> and restart Visual Studio to fall back to the VSTest adapter.
Everything below was measured on .NET SDK 10.0.201 (runtime 10.0.5) with xunit.v3 3.2.2, xunit.runner.visualstudio 3.1.5, Microsoft.NET.Test.Sdk 17.14.1, and Microsoft.Testing.Platform 1.9.1. The mechanism is unchanged on .NET 11 previews, because it lives in the xUnit and MTP MSBuild targets rather than in the SDK.
The symptoms this post covers
Unable to connect to testing platform runner process [MyProject.Tests.dll]
Or no error at all: the Test Explorer spinner turns, the test count stays at zero or freezes partway through a solution-wide run, and a MyProject.Tests.exe process sits idle in Task Manager until you stop the run. Meanwhile:
> dotnet test
Test run summary: Passed!
total: 2
failed: 0
succeeded: 2
Why Test Explorer and dotnet test do not run the same code
xUnit v3 test projects are executables (<OutputType>Exe</OutputType>), and xunit.v3.msbuildtasks generates their Main for you into obj/Debug/net10.0/XunitAutoGeneratedEntryPoint.cs. Open that file in any v3 project and the whole problem is on one line:
// 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();
}
Two runners, one binary. dotnet test, dotnet run, and double-clicking the exe all take the else branch and use xUnit’s in-process console runner. Visual Studio takes the if branch.
What Visual Studio passes looks like this, and you can run it by hand:
# .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()
The direction matters. Visual Studio listens on a loopback TCP port and the test process dials back into it. Discovery and execution are then JSON-RPC requests such as testing/discoverTests and testing/runTests sent over that socket. Anything that stops the process from completing that handshake, or from answering afterwards, shows up as a hang and never as a test failure, because no test ever ran.
Visual Studio only tries this at all when the project advertises the capability. That comes from Microsoft.Testing.Platform.targets in the microsoft.testing.platform package:
<!-- 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 is the switch. Current Visual Studio ships with the Microsoft.Testing.Platform Test Explorer experience enabled by default, so any project carrying that capability gets driven through server mode whether or not you asked for it.
The minimal repro
A stock xUnit v3 project, nothing exotic:
<!-- .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>
Inspecting what that project actually declares is a one-liner, and it is the first thing to run when Test Explorer misbehaves:
# .NET SDK 10.0.201
> dotnet msbuild -getItem:ProjectCapability
On the project above you get both TestingPlatformServer and TestContainer. That is a project wired for MTP server mode and, simultaneously, carrying the full VSTest stack: the output folder contains Microsoft.Testing.Platform.dll next to Microsoft.VisualStudio.TestPlatform.Common.dll, testhost.exe, and xunit.runner.visualstudio.testadapter.dll. Two test platforms in one bin folder is legal, but it means the runner you exercise from the CLI is not necessarily the one the IDE picks.
Fix 1: make both code paths the same, which is the real fix
Set one property and the generated entry point inverts:
<!-- Directory.Build.props, .NET SDK 10.0.201, xunit.v3 3.2.2 -->
<Project>
<PropertyGroup>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
</PropertyGroup>
</Project>
Rebuild and read XunitAutoGeneratedEntryPoint.cs again:
// 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 is now the default path for every invocation, so a passing local run genuinely exercises the code Test Explorer uses. It also gives you the MTP command line, which is where the diagnostics live. --diagnostic is an MTP option, not an xUnit one, and before this change the exe rejects it with error: unknown option: --diagnostic:
# .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
That .diag file is what to read when server mode stalls. It records the platform startup sequence and every JSON-RPC message, so you can see whether the process never connected, connected and never received testing/runTests, or received it and blocked inside a test.
Pair this with MTP mode for dotnet test in global.json, so the CLI stops going through the VSTest bridge entirely:
{
"test": { "runner": "Microsoft.Testing.Platform" }
}
In MTP mode you no longer need TestingPlatformDotnetTestSupport, and MTP arguments no longer need the extra -- separator.
Fix 2: turn the capability off and fall back to VSTest
When you need a working Test Explorer immediately, remove the capability instead:
<!-- Directory.Build.props. Restart Visual Studio after changing this. -->
<PropertyGroup>
<DisableTestingPlatformServerCapability>true</DisableTestingPlatformServerCapability>
</PropertyGroup>
This is a documented xUnit workaround, and it does exactly what the targets say: TestingPlatformServer disappears from the capability list, TestContainer survives because Microsoft.NET.Test.Sdk also contributes it, and Visual Studio goes back to discovering tests through xunit.runner.visualstudio. Verify rather than assume:
> dotnet msbuild -p:DisableTestingPlatformServerCapability=true -getItem:ProjectCapability
The restart is not optional. Project capabilities are read when the project is loaded, so a rebuild alone leaves the running IDE on the old decision.
Fix 3: delete your hand-written Main
This is the cause worth checking before the others, because it produces exactly the reported signature: CLI green, Test Explorer dead. Adding a Program.cs with top-level statements to an xUnit v3 project does not replace part of the generated entry point, it replaces all of it, and the compiler only warns:
warning CS7022: The entry point of the program is global code;
ignoring 'XunitAutoGeneratedEntryPoint.Main(string[])' entry point.
A build with that warning still succeeds. Here is a Program.cs that looks entirely reasonable, wiring up xUnit’s console runner by hand:
// .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);
Run it with no arguments and two tests pass. Run it the way Visual Studio does and the --server handling is simply gone:
> 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
The process prints to stdout and exits 3 without ever opening the socket. From Visual Studio’s side that is a runner that connected to nothing, which surfaces as the connect error or as an indefinite wait. If you genuinely need a custom entry point, set <GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint>, build against Microsoft.Testing.Platform’s TestApplication.CreateBuilderAsync(args), and forward args verbatim. Swallowing the argument array is the bug.
Fix 4: keep one Microsoft.Testing.Platform major version per solution
xUnit ships variant packages that pin different MTP majors, and mixing them across a solution is what produces the “some projects run, some randomly do not” pattern in a solution-wide run. Measured resolutions at xunit.v3 3.2.2:
| Package reference | Microsoft.Testing.Platform resolved |
|---|---|
xunit.v3 | 1.9.1 |
xunit.v3.mtp-v2 | 2.0.2 |
xunit.v3.mtp-off | none, VSTest only |
Pick one and set it once in Directory.Build.props rather than per project. Microsoft’s own guidance on TestingPlatformDotnetTestSupport makes the same point for a different property: a solution where some projects use one platform and others use the other “might not work correctly and is an unsupported scenario”. Adding a new test project from a template is the usual way a mismatch sneaks in.
Why dotnet test going green proves less than you think
There is a worse variant of this: dotnet test can exit 0 having run nothing at all. Take an MTP-only project, no Microsoft.NET.Test.Sdk, no xunit.runner.visualstudio, and no test section in global.json. dotnet test defaults to VSTest mode, finds no VSTest adapter, and reports success:
# .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
Two tests exist. Zero ran. Exit code 0. Add the global.json runner section from Fix 1 and the same command reports total: 2, succeeded: 2. If your CI is currently green on an xUnit v3 project, confirm the test count in the log before trusting it. --minimum-expected-tests via TestingPlatformCommandLineArguments is the cheap guard:
<PropertyGroup>
<TestingPlatformCommandLineArguments>--minimum-expected-tests 1</TestingPlatformCommandLineArguments>
</PropertyGroup>
Lookalikes that are not this bug
Not every stalled Test Explorer run is server mode. Rank these after the four fixes above:
- A test that actually deadlocks. If discovery completes and the count freezes mid-run on the same test every time, you have a blocking
.Resultor.Wait()on an async call, not a transport problem. The.diaglog will showtesting/runTestsarriving. DisableTestingPlatformServerCapabilityon an MTP-only project. Measured: with no VSTest packages present, that property strips bothTestingPlatformServerandTestContainer, leaving only the two sub-capabilitiesTestingPlatformServer.ExitOnProcessExitCapabilityandTestingPlatformServer.UseListTestsOptionForDiscoveryCapability. The project then vanishes from Test Explorer entirely rather than falling back. Fix 2 only works whileMicrosoft.NET.Test.Sdkis still referenced.- A
.runsettingsfile selected in Test Explorer. Server mode and run settings have their own interaction problems; deselect it under Test > Configure Run Settings before blaming the platform. - Missing transitive assemblies. A testhost that dies during startup because an assembly in
deps.jsoncannot be found looks identical from the IDE. The Windows Application event log and the.diagfile separate the two in seconds. - VS Code. The C# Dev Kit reads the same
TestingPlatformServercapability, so the same four fixes apply, but its logs live in the C# Dev Kit output channel rather than inTestResults.
Related
If you are still deciding which framework to standardise on, the measured comparison of xUnit v3, NUnit, and MSTest covers the same MTP-versus-VSTest packaging split from the framework-choice angle. Once your suite runs reliably, MTP 2.3’s GitHub Actions reporting turns failures into annotations on the pull request diff. For the integration-test layer, there is a walkthrough of WebApplicationFactory in ASP.NET Core 11 and a comparison of WebApplicationFactory against Testcontainers. If your CI broke around test tooling for an unrelated reason, VSTest dropping Newtonsoft.Json is the other change that bit people this year.
Sources
- Microsoft Testing Platform, xUnit.net v3 documentation for
UseMicrosoftTestingPlatformRunner,DisableTestingPlatformServerCapability, and themtp-v1/mtp-v2/mtp-offpackage variants. - Testing with dotnet test, Microsoft Learn for VSTest mode versus MTP mode,
TestingPlatformDotnetTestSupport, theglobal.jsonrunner section, andTestingPlatformCommandLineArguments. - xunit/xunit issue 3519, a 34-project solution on Visual Studio 18.4.0 where some test hosts never receive the
testing/runTestsrequest. - microsoft/testfx issue 4729 for “Unable to connect to testing platform runner process” under server mode.
Microsoft.Testing.Platform.targetsfrom themicrosoft.testing.platform2.3.3 package, which is where theTestingPlatformServercapability is declared.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.