Solución: el Explorador de pruebas de Visual Studio se cuelga en un proyecto xUnit v3 mientras dotnet test pasa
El Explorador de pruebas ejecuta xUnit v3 a través del modo servidor de Microsoft.Testing.Platform sobre un socket JSON-RPC, dotnet test no. Configura UseMicrosoftTestingPlatformRunner para que ambas rutas sean el mismo código, o DisableTestingPlatformServerCapability para volver a VSTest.
El Explorador de pruebas y dotnet test no ejecutan el mismo código. Un proyecto de pruebas xUnit v3 compila un ejecutable cuyo punto de entrada generado se bifurca según un único argumento: si Visual Studio pasa --server, el proceso cede el control a Microsoft.Testing.Platform e intenta abrir un socket JSON-RPC de vuelta al IDE; en caso contrario, ejecuta el runner de consola en proceso propio de xUnit. Una ejecución verde desde la línea de comandos no te dice nada sobre la ruta del modo servidor. La solución más rápida es hacer que ambas rutas sean idénticas agregando <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>. Si necesitas el Explorador de pruebas funcionando ahora mismo, agrega <DisableTestingPlatformServerCapability>true</DisableTestingPlatformServerCapability> y reinicia Visual Studio para volver al adaptador de VSTest.
Todo lo que sigue se midió en .NET SDK 10.0.201 (runtime 10.0.5) con xunit.v3 3.2.2, xunit.runner.visualstudio 3.1.5, Microsoft.NET.Test.Sdk 17.14.1 y Microsoft.Testing.Platform 1.9.1. El mecanismo no cambia en las versiones preliminares de .NET 11, porque vive en los targets de MSBuild de xUnit y MTP, no en el SDK.
Los síntomas que cubre este artículo
Unable to connect to testing platform runner process [MyProject.Tests.dll]
O ningún error en absoluto: el indicador de progreso del Explorador de pruebas gira, el conteo de pruebas se queda en cero o se congela a mitad de una ejecución de toda la solución, y un proceso MyProject.Tests.exe queda inactivo en el Administrador de tareas hasta que detienes la ejecución. Mientras tanto:
> dotnet test
Test run summary: Passed!
total: 2
failed: 0
succeeded: 2
Por qué el Explorador de pruebas y dotnet test no ejecutan el mismo código
Los proyectos de pruebas xUnit v3 son ejecutables (<OutputType>Exe</OutputType>), y xunit.v3.msbuildtasks genera su Main por ti dentro de obj/Debug/net10.0/XunitAutoGeneratedEntryPoint.cs. Abre ese archivo en cualquier proyecto v3 y todo el problema está en una sola línea:
// 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();
}
Dos runners, un binario. dotnet test, dotnet run y hacer doble clic en el ejecutable toman la rama else y usan el runner de consola en proceso de xUnit. Visual Studio toma la rama if.
Lo que pasa Visual Studio se ve así, y puedes ejecutarlo a mano:
# .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()
La dirección importa. Visual Studio escucha en un puerto TCP de loopback y el proceso de pruebas marca hacia él. El descubrimiento y la ejecución son entonces solicitudes JSON-RPC como testing/discoverTests y testing/runTests enviadas por ese socket. Cualquier cosa que impida al proceso completar ese handshake, o responder después, aparece como un cuelgue y nunca como una prueba fallida, porque ninguna prueba llegó a ejecutarse.
Visual Studio solo intenta esto cuando el proyecto anuncia la capacidad. Eso viene de Microsoft.Testing.Platform.targets, en el paquete 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 es el interruptor. Las versiones actuales de Visual Studio traen la experiencia de Explorador de pruebas de Microsoft.Testing.Platform activada por omisión, así que cualquier proyecto que lleve esa capacidad se ejecuta a través del modo servidor, lo hayas pedido o no.
La reproducción mínima
Un proyecto xUnit v3 estándar, nada exótico:
<!-- .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>
Inspeccionar lo que ese proyecto declara realmente es cuestión de una línea, y es lo primero que hay que ejecutar cuando el Explorador de pruebas se comporta mal:
# .NET SDK 10.0.201
> dotnet msbuild -getItem:ProjectCapability
En el proyecto de arriba obtienes tanto TestingPlatformServer como TestContainer. Ese es un proyecto cableado para el modo servidor de MTP y que, al mismo tiempo, carga la pila completa de VSTest: la carpeta de salida contiene Microsoft.Testing.Platform.dll junto a Microsoft.VisualStudio.TestPlatform.Common.dll, testhost.exe y xunit.runner.visualstudio.testadapter.dll. Dos plataformas de pruebas en una carpeta bin es legal, pero significa que el runner que ejercitas desde la línea de comandos no es necesariamente el que elige el IDE.
Solución 1: hacer que ambas rutas de código sean la misma, que es la solución real
Configura una propiedad y el punto de entrada generado se invierte:
<!-- Directory.Build.props, .NET SDK 10.0.201, xunit.v3 3.2.2 -->
<Project>
<PropertyGroup>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
</PropertyGroup>
</Project>
Vuelve a compilar y lee XunitAutoGeneratedEntryPoint.cs otra vez:
// 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 es ahora la ruta por omisión para cada invocación, así que una ejecución local exitosa realmente ejercita el código que usa el Explorador de pruebas. También te da la línea de comandos de MTP, que es donde viven los diagnósticos. --diagnostic es una opción de MTP, no de xUnit, y antes de este cambio el ejecutable la rechaza con 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
Ese archivo .diag es lo que hay que leer cuando el modo servidor se atasca. Registra la secuencia de arranque de la plataforma y cada mensaje JSON-RPC, así que puedes ver si el proceso nunca se conectó, si se conectó y nunca recibió testing/runTests, o si lo recibió y se bloqueó dentro de una prueba.
Combina esto con el modo MTP para dotnet test en global.json, para que la línea de comandos deje de pasar por el puente de VSTest:
{
"test": { "runner": "Microsoft.Testing.Platform" }
}
En modo MTP ya no necesitas TestingPlatformDotnetTestSupport, y los argumentos de MTP ya no necesitan el separador -- adicional.
Solución 2: desactivar la capacidad y volver a VSTest
Cuando necesitas un Explorador de pruebas funcional de inmediato, elimina la capacidad en su lugar:
<!-- Directory.Build.props. Restart Visual Studio after changing this. -->
<PropertyGroup>
<DisableTestingPlatformServerCapability>true</DisableTestingPlatformServerCapability>
</PropertyGroup>
Esta es una solución alternativa documentada por xUnit, y hace exactamente lo que dicen los targets: TestingPlatformServer desaparece de la lista de capacidades, TestContainer sobrevive porque Microsoft.NET.Test.Sdk también la aporta, y Visual Studio vuelve a descubrir pruebas mediante xunit.runner.visualstudio. Verifica en lugar de suponer:
> dotnet msbuild -p:DisableTestingPlatformServerCapability=true -getItem:ProjectCapability
El reinicio no es opcional. Las capacidades de proyecto se leen cuando se carga el proyecto, así que recompilar por sí solo deja al IDE en ejecución con la decisión anterior.
Solución 3: elimina tu Main escrito a mano
Esta es la causa que conviene revisar antes que las otras, porque produce exactamente la firma reportada: línea de comandos en verde, Explorador de pruebas muerto. Agregar un Program.cs con instrucciones de nivel superior a un proyecto xUnit v3 no reemplaza parte del punto de entrada generado, lo reemplaza entero, y el compilador solo advierte:
warning CS7022: The entry point of the program is global code;
ignoring 'XunitAutoGeneratedEntryPoint.Main(string[])' entry point.
Una compilación con esa advertencia igual tiene éxito. Aquí hay un Program.cs que parece del todo razonable, conectando a mano el runner de consola de xUnit:
// .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);
Ejecútalo sin argumentos y dos pruebas pasan. Ejecútalo como lo hace Visual Studio y el manejo de --server simplemente no está:
> 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
El proceso escribe en stdout y sale con código 3 sin abrir nunca el socket. Desde el lado de Visual Studio eso es un runner que no se conectó a nada, lo que se manifiesta como el error de conexión o como una espera indefinida. Si de verdad necesitas un punto de entrada propio, configura <GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint>, compila contra TestApplication.CreateBuilderAsync(args) de Microsoft.Testing.Platform y reenvía args literalmente. Tragarse el arreglo de argumentos es el error.
Solución 4: mantén una sola versión mayor de Microsoft.Testing.Platform por solución
xUnit publica paquetes variantes que fijan distintas versiones mayores de MTP, y mezclarlos dentro de una solución es lo que produce el patrón de “algunos proyectos se ejecutan, otros aleatoriamente no” en una ejecución de toda la solución. Resoluciones medidas con xunit.v3 3.2.2:
| Referencia de paquete | Microsoft.Testing.Platform resuelto |
|---|---|
xunit.v3 | 1.9.1 |
xunit.v3.mtp-v2 | 2.0.2 |
xunit.v3.mtp-off | ninguna, solo VSTest |
Elige una y configúrala una sola vez en Directory.Build.props en lugar de por proyecto. La propia guía de Microsoft sobre TestingPlatformDotnetTestSupport señala lo mismo para otra propiedad: una solución donde unos proyectos usan una plataforma y otros la otra “podría no funcionar correctamente y es un escenario no admitido”. Agregar un nuevo proyecto de pruebas desde una plantilla es la forma habitual en que se cuela un desajuste.
Por qué un dotnet test en verde prueba menos de lo que crees
Hay una variante peor de esto: dotnet test puede salir con código 0 sin haber ejecutado nada. Toma un proyecto solo-MTP, sin Microsoft.NET.Test.Sdk, sin xunit.runner.visualstudio y sin sección test en global.json. dotnet test usa por omisión el modo VSTest, no encuentra ningún adaptador de VSTest y reporta éxito:
# .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
Existen dos pruebas. Se ejecutaron cero. Código de salida 0. Agrega la sección de runner de global.json de la Solución 1 y el mismo comando reporta total: 2, succeeded: 2. Si tu CI está actualmente en verde sobre un proyecto xUnit v3, confirma el conteo de pruebas en el log antes de confiar en él. --minimum-expected-tests a través de TestingPlatformCommandLineArguments es la protección barata:
<PropertyGroup>
<TestingPlatformCommandLineArguments>--minimum-expected-tests 1</TestingPlatformCommandLineArguments>
</PropertyGroup>
Casos parecidos que no son este error
No toda ejecución atascada del Explorador de pruebas es modo servidor. Clasifica estos después de las cuatro soluciones anteriores:
- Una prueba que realmente se interbloquea. Si el descubrimiento termina y el conteo se congela a mitad de ejecución siempre en la misma prueba, tienes un
.Resulto.Wait()bloqueante sobre una llamada asíncrona, no un problema de transporte. El log.diagmostrará quetesting/runTestsllegó. DisableTestingPlatformServerCapabilityen un proyecto solo-MTP. Medido: sin paquetes de VSTest presentes, esa propiedad elimina tantoTestingPlatformServercomoTestContainer, dejando solo las dos subcapacidadesTestingPlatformServer.ExitOnProcessExitCapabilityyTestingPlatformServer.UseListTestsOptionForDiscoveryCapability. El proyecto entonces desaparece por completo del Explorador de pruebas en vez de recurrir a un plan B. La Solución 2 solo funciona mientrasMicrosoft.NET.Test.Sdksiga referenciado.- Un archivo
.runsettingsseleccionado en el Explorador de pruebas. El modo servidor y los archivos de configuración de ejecución tienen sus propios problemas de interacción; deselecciónalo en Prueba > Configurar opciones de ejecución antes de culpar a la plataforma. - Ensamblados transitivos faltantes. Un testhost que muere durante el arranque porque un ensamblado del
deps.jsonno se encuentra se ve idéntico desde el IDE. El registro de eventos de aplicación de Windows y el archivo.diagseparan ambos casos en segundos. - VS Code. El C# Dev Kit lee la misma capacidad
TestingPlatformServer, así que aplican las mismas cuatro soluciones, pero sus logs viven en el canal de salida del C# Dev Kit en lugar de enTestResults.
Relacionado
Si todavía estás decidiendo en qué framework estandarizarte, la comparación medida de xUnit v3, NUnit y MSTest cubre la misma separación de empaquetado entre MTP y VSTest desde el ángulo de la elección de framework. Una vez que tu suite corre de forma confiable, los reportes de MTP 2.3 para GitHub Actions convierten los fallos en anotaciones sobre el diff del pull request. Para la capa de pruebas de integración, hay un recorrido por WebApplicationFactory en ASP.NET Core 11 y una comparación de WebApplicationFactory frente a Testcontainers. Si tu CI se rompió alrededor del herramental de pruebas por una razón no relacionada, VSTest quitando Newtonsoft.Json es el otro cambio que mordió a mucha gente este año.
Fuentes
- Microsoft Testing Platform, documentación de xUnit.net v3 para
UseMicrosoftTestingPlatformRunner,DisableTestingPlatformServerCapabilityy las variantes de paquetemtp-v1/mtp-v2/mtp-off. - Pruebas con dotnet test, Microsoft Learn para el modo VSTest frente al modo MTP,
TestingPlatformDotnetTestSupport, la sección de runner deglobal.jsonyTestingPlatformCommandLineArguments. - xunit/xunit issue 3519, una solución de 34 proyectos en Visual Studio 18.4.0 donde algunos test hosts nunca reciben la solicitud
testing/runTests. - microsoft/testfx issue 4729 para “Unable to connect to testing platform runner process” bajo modo servidor.
Microsoft.Testing.Platform.targetsdel paquetemicrosoft.testing.platform2.3.3, que es donde se declara la capacidadTestingPlatformServer.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.