Start Debugging

Correção: o Gerenciador de Testes do Visual Studio trava em um projeto xUnit v3 enquanto o dotnet test passa

O Gerenciador de Testes executa o xUnit v3 pelo modo servidor do Microsoft.Testing.Platform sobre um socket JSON-RPC, o dotnet test não. Configure UseMicrosoftTestingPlatformRunner para que os dois caminhos sejam o mesmo código, ou DisableTestingPlatformServerCapability para voltar ao VSTest.

O Gerenciador de Testes e o dotnet test não executam o mesmo código. Um projeto de testes xUnit v3 compila um executável cujo ponto de entrada gerado se ramifica com base em um único argumento: se o Visual Studio passa --server, o processo entrega o controle ao Microsoft.Testing.Platform e tenta abrir um socket JSON-RPC de volta para a IDE; caso contrário, executa o runner de console em processo do próprio xUnit. Uma execução verde na linha de comando não diz nada sobre o caminho do modo servidor. A correção mais rápida é tornar os dois caminhos idênticos adicionando <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>. Se você precisa do Gerenciador de Testes funcionando agora, adicione <DisableTestingPlatformServerCapability>true</DisableTestingPlatformServerCapability> e reinicie o Visual Studio para voltar ao adaptador do VSTest.

Tudo abaixo foi medido no .NET SDK 10.0.201 (runtime 10.0.5) com xunit.v3 3.2.2, xunit.runner.visualstudio 3.1.5, Microsoft.NET.Test.Sdk 17.14.1 e Microsoft.Testing.Platform 1.9.1. O mecanismo é o mesmo nas versões prévias do .NET 11, porque ele vive nos targets do MSBuild do xUnit e do MTP, e não no SDK.

Os sintomas que este artigo cobre

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

Ou nenhum erro: o indicador de progresso do Gerenciador de Testes gira, a contagem de testes fica em zero ou congela no meio de uma execução da solução inteira, e um processo MyProject.Tests.exe fica parado no Gerenciador de Tarefas até você interromper a execução. Enquanto isso:

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

Por que o Gerenciador de Testes e o dotnet test não executam o mesmo código

Projetos de testes xUnit v3 são executáveis (<OutputType>Exe</OutputType>), e o xunit.v3.msbuildtasks gera o Main deles para você dentro de obj/Debug/net10.0/XunitAutoGeneratedEntryPoint.cs. Abra esse arquivo em qualquer projeto v3 e o problema inteiro está em uma única linha:

// 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();
}

Dois runners, um binário. dotnet test, dotnet run e dar dois cliques no executável seguem o ramo else e usam o runner de console em processo do xUnit. O Visual Studio segue o ramo if.

O que o Visual Studio passa se parece com isto, e você pode executar na mão:

# .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()

A direção importa. O Visual Studio escuta em uma porta TCP de loopback e o processo de testes disca de volta para ela. Descoberta e execução são então requisições JSON-RPC como testing/discoverTests e testing/runTests enviadas por esse socket. Qualquer coisa que impeça o processo de completar esse handshake, ou de responder depois, aparece como um travamento e nunca como uma falha de teste, porque nenhum teste chegou a rodar.

O Visual Studio só tenta isso quando o projeto anuncia a capacidade. Ela vem de Microsoft.Testing.Platform.targets, no pacote 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 é a chave. As versões atuais do Visual Studio vêm com a experiência de Gerenciador de Testes do Microsoft.Testing.Platform ligada por padrão, então qualquer projeto que carregue essa capacidade é conduzido pelo modo servidor, quer você tenha pedido ou não.

A reprodução mínima

Um projeto xUnit v3 comum, 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>

Inspecionar o que esse projeto de fato declara é uma linha só, e é a primeira coisa a executar quando o Gerenciador de Testes se comporta mal:

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

No projeto acima você obtém tanto TestingPlatformServer quanto TestContainer. Esse é um projeto ligado ao modo servidor do MTP e que, ao mesmo tempo, carrega a pilha completa do VSTest: a pasta de saída contém Microsoft.Testing.Platform.dll ao lado de Microsoft.VisualStudio.TestPlatform.Common.dll, testhost.exe e xunit.runner.visualstudio.testadapter.dll. Duas plataformas de teste em uma pasta bin é permitido, mas significa que o runner que você exercita pela linha de comando não é necessariamente o que a IDE escolhe.

Correção 1: fazer os dois caminhos de código serem o mesmo, que é a correção de verdade

Configure uma propriedade e o ponto de entrada gerado se inverte:

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

Compile de novo e leia o XunitAutoGeneratedEntryPoint.cs outra 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();
}

O Microsoft.Testing.Platform agora é o caminho padrão para toda invocação, então uma execução local que passa realmente exercita o código que o Gerenciador de Testes usa. Isso também te dá a linha de comando do MTP, que é onde vivem os diagnósticos. --diagnostic é uma opção do MTP, não do xUnit, e antes dessa mudança o executável a rejeita com 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

Esse arquivo .diag é o que ler quando o modo servidor empaca. Ele registra a sequência de inicialização da plataforma e cada mensagem JSON-RPC, então você consegue ver se o processo nunca conectou, se conectou e nunca recebeu testing/runTests, ou se recebeu e bloqueou dentro de um teste.

Combine isso com o modo MTP para o dotnet test no global.json, para que a linha de comando pare de passar pela ponte do VSTest:

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

No modo MTP você não precisa mais de TestingPlatformDotnetTestSupport, e os argumentos do MTP não precisam mais do separador -- extra.

Correção 2: desligar a capacidade e voltar ao VSTest

Quando você precisa de um Gerenciador de Testes funcionando imediatamente, remova a capacidade:

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

Essa é uma solução alternativa documentada pelo xUnit, e faz exatamente o que os targets dizem: TestingPlatformServer some da lista de capacidades, TestContainer sobrevive porque o Microsoft.NET.Test.Sdk também a contribui, e o Visual Studio volta a descobrir testes pelo xunit.runner.visualstudio. Verifique em vez de supor:

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

O reinício não é opcional. Capacidades de projeto são lidas quando o projeto é carregado, então recompilar sozinho deixa a IDE em execução com a decisão antiga.

Correção 3: apague o seu Main escrito à mão

Essa é a causa que vale checar antes das outras, porque produz exatamente a assinatura relatada: linha de comando verde, Gerenciador de Testes morto. Adicionar um Program.cs com instruções de nível superior a um projeto xUnit v3 não substitui parte do ponto de entrada gerado, substitui ele inteiro, e o compilador apenas avisa:

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

Uma compilação com esse aviso ainda tem sucesso. Aqui está um Program.cs que parece totalmente razoável, montando o runner de console do xUnit na mão:

// .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);

Execute sem argumentos e dois testes passam. Execute do jeito que o Visual Studio faz e o tratamento de --server simplesmente não existe:

> 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

O processo escreve no stdout e sai com código 3 sem nunca abrir o socket. Do lado do Visual Studio isso é um runner que não conectou em nada, o que aparece como o erro de conexão ou como uma espera indefinida. Se você realmente precisa de um ponto de entrada próprio, configure <GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint>, compile contra o TestApplication.CreateBuilderAsync(args) do Microsoft.Testing.Platform e repasse args literalmente. Engolir o array de argumentos é o bug.

Correção 4: mantenha uma única versão maior do Microsoft.Testing.Platform por solução

O xUnit publica pacotes variantes que fixam versões maiores diferentes do MTP, e misturá-los em uma solução é o que produz o padrão de “alguns projetos rodam, outros aleatoriamente não” numa execução da solução inteira. Resoluções medidas com xunit.v3 3.2.2:

Referência de pacoteMicrosoft.Testing.Platform resolvido
xunit.v31.9.1
xunit.v3.mtp-v22.0.2
xunit.v3.mtp-offnenhuma, só VSTest

Escolha uma e configure uma vez só no Directory.Build.props em vez de por projeto. A própria orientação da Microsoft sobre TestingPlatformDotnetTestSupport faz a mesma observação para outra propriedade: uma solução em que alguns projetos usam uma plataforma e outros usam a outra “pode não funcionar corretamente e é um cenário sem suporte”. Adicionar um novo projeto de testes a partir de um template é o jeito usual de um descompasso entrar sem ninguém notar.

Por que um dotnet test verde prova menos do que você pensa

Existe uma variante pior disso: o dotnet test pode sair com código 0 sem ter rodado nada. Pegue um projeto só-MTP, sem Microsoft.NET.Test.Sdk, sem xunit.runner.visualstudio e sem seção test no global.json. O dotnet test usa o modo VSTest por padrão, não encontra nenhum adaptador do VSTest e reporta sucesso:

# .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

Existem dois testes. Zero rodaram. Código de saída 0. Adicione a seção de runner do global.json da Correção 1 e o mesmo comando reporta total: 2, succeeded: 2. Se o seu CI está verde hoje em um projeto xUnit v3, confirme a contagem de testes no log antes de confiar nela. --minimum-expected-tests via TestingPlatformCommandLineArguments é a proteção barata:

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

Casos parecidos que não são este bug

Nem toda execução travada do Gerenciador de Testes é modo servidor. Classifique estes depois das quatro correções acima:

Relacionado

Se você ainda está decidindo em qual framework padronizar, a comparação medida entre xUnit v3, NUnit e MSTest cobre a mesma divisão de empacotamento entre MTP e VSTest pelo ângulo da escolha de framework. Depois que sua suíte roda de forma confiável, os relatórios do MTP 2.3 para GitHub Actions transformam falhas em anotações no diff do pull request. Para a camada de testes de integração, há um passo a passo sobre WebApplicationFactory no ASP.NET Core 11 e uma comparação de WebApplicationFactory contra Testcontainers. Se o seu CI quebrou em torno do ferramental de testes por um motivo não relacionado, o VSTest removendo o Newtonsoft.Json é a outra mudança que pegou muita gente este ano.

Fontes

Comments

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

< Voltar