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 pacote | Microsoft.Testing.Platform resolvido |
|---|---|
xunit.v3 | 1.9.1 |
xunit.v3.mtp-v2 | 2.0.2 |
xunit.v3.mtp-off | nenhuma, 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:
- Um teste que realmente entra em deadlock. Se a descoberta termina e a contagem congela no meio da execução sempre no mesmo teste, você tem um
.Resultou.Wait()bloqueante sobre uma chamada assíncrona, não um problema de transporte. O log.diagvai mostrartesting/runTestschegando. DisableTestingPlatformServerCapabilityem um projeto só-MTP. Medido: sem pacotes do VSTest presentes, essa propriedade remove tantoTestingPlatformServerquantoTestContainer, deixando só as duas subcapacidadesTestingPlatformServer.ExitOnProcessExitCapabilityeTestingPlatformServer.UseListTestsOptionForDiscoveryCapability. O projeto então some por completo do Gerenciador de Testes em vez de cair num plano B. A Correção 2 só funciona enquanto oMicrosoft.NET.Test.Sdkcontinuar referenciado.- Um arquivo
.runsettingsselecionado no Gerenciador de Testes. O modo servidor e os arquivos de configuração de execução têm problemas de interação próprios; desmarque em Teste > Configurar Configurações de Execução antes de culpar a plataforma. - Assemblies transitivos faltando. Um testhost que morre durante a inicialização porque um assembly do
deps.jsonnão é encontrado parece idêntico visto da IDE. O log de eventos de aplicativo do Windows e o arquivo.diagseparam os dois casos em segundos. - VS Code. O C# Dev Kit lê a mesma capacidade
TestingPlatformServer, então as mesmas quatro correções valem, mas os logs dele ficam no canal de saída do C# Dev Kit em vez de emTestResults.
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
- Microsoft Testing Platform, documentação do xUnit.net v3 para
UseMicrosoftTestingPlatformRunner,DisableTestingPlatformServerCapabilitye as variantes de pacotemtp-v1/mtp-v2/mtp-off. - Testando com dotnet test, Microsoft Learn para o modo VSTest contra o modo MTP,
TestingPlatformDotnetTestSupport, a seção de runner doglobal.jsoneTestingPlatformCommandLineArguments. - xunit/xunit issue 3519, uma solução de 34 projetos no Visual Studio 18.4.0 onde alguns test hosts nunca recebem a requisição
testing/runTests. - microsoft/testfx issue 4729 para “Unable to connect to testing platform runner process” sob modo servidor.
Microsoft.Testing.Platform.targetsdo pacotemicrosoft.testing.platform2.3.3, que é onde a capacidadeTestingPlatformServeré declarada.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.