Start Debugging

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 paqueteMicrosoft.Testing.Platform resuelto
xunit.v31.9.1
xunit.v3.mtp-v22.0.2
xunit.v3.mtp-offninguna, 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:

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

Comments

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

< Volver