Исправление: обозреватель тестов Visual Studio зависает на проекте xUnit v3, хотя dotnet test проходит
Обозреватель тестов запускает xUnit v3 через серверный режим Microsoft.Testing.Platform по сокету JSON-RPC, а dotnet test нет. Задайте UseMicrosoftTestingPlatformRunner, чтобы оба пути были одним кодом, либо DisableTestingPlatformServerCapability для возврата к VSTest.
Обозреватель тестов и dotnet test выполняют не один и тот же код. Тестовый проект xUnit v3 собирается в исполняемый файл, чья сгенерированная точка входа ветвится по одному-единственному аргументу: если Visual Studio передаёт --server, процесс отдаёт управление Microsoft.Testing.Platform и пытается открыть сокет JSON-RPC обратно к IDE; иначе запускается собственный внутрипроцессный консольный runner xUnit. Зелёный запуск из командной строки ничего не говорит о серверном пути. Самое быстрое исправление состоит в том, чтобы сделать оба пути одинаковыми, добавив <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>. Если обозреватель тестов нужен прямо сейчас, добавьте <DisableTestingPlatformServerCapability>true</DisableTestingPlatformServerCapability> и перезапустите Visual Studio, чтобы вернуться к адаптеру VSTest.
Всё изложенное ниже измерено на .NET SDK 10.0.201 (среда выполнения 10.0.5) с xunit.v3 3.2.2, xunit.runner.visualstudio 3.1.5, Microsoft.NET.Test.Sdk 17.14.1 и Microsoft.Testing.Platform 1.9.1. В предварительных версиях .NET 11 механизм не меняется, поскольку он находится в MSBuild-таргетах xUnit и MTP, а не в SDK.
Симптомы, которым посвящена статья
Unable to connect to testing platform runner process [MyProject.Tests.dll]
Либо ошибки нет вовсе: индикатор в обозревателе тестов крутится, счётчик тестов остаётся нулевым или замирает посреди запуска по всему решению, а процесс MyProject.Tests.exe бездействует в диспетчере задач, пока запуск не остановят. При этом:
> dotnet test
Test run summary: Passed!
total: 2
failed: 0
succeeded: 2
Почему обозреватель тестов и dotnet test выполняют разный код
Тестовые проекты xUnit v3 являются исполняемыми файлами (<OutputType>Exe</OutputType>), а xunit.v3.msbuildtasks генерирует для них Main в файл obj/Debug/net10.0/XunitAutoGeneratedEntryPoint.cs. Откройте этот файл в любом проекте v3, и вся суть проблемы уместится в одну строку:
// 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();
}
Два runner-а, один бинарный файл. dotnet test, dotnet run и двойной щелчок по exe идут по ветке else и используют внутрипроцессный консольный runner xUnit. Visual Studio идёт по ветке if.
То, что передаёт Visual Studio, выглядит так, и это можно запустить вручную:
# .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()
Направление здесь важно. Visual Studio слушает TCP-порт на петлевом интерфейсе, а тестовый процесс сам подключается к нему. Обнаружение и выполнение после этого представляют собой запросы JSON-RPC вида testing/discoverTests и testing/runTests, отправляемые по этому сокету. Всё, что мешает процессу завершить рукопожатие или отвечать после него, проявляется как зависание и никогда как упавший тест, потому что ни один тест так и не выполнился.
Visual Studio вообще пробует такой путь только тогда, когда проект объявляет соответствующую возможность. Она берётся из Microsoft.Testing.Platform.targets в пакете 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 и есть переключатель. Текущие версии Visual Studio поставляются с включённым по умолчанию режимом обозревателя тестов на основе Microsoft.Testing.Platform, поэтому любой проект с этой возможностью управляется через серверный режим независимо от того, просили вы об этом или нет.
Минимальное воспроизведение
Обычный проект xUnit v3, ничего необычного:
<!-- .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>
Проверить, что этот проект объявляет на самом деле, можно одной строкой, и это первое, что стоит выполнить, когда обозреватель тестов ведёт себя странно:
# .NET SDK 10.0.201
> dotnet msbuild -getItem:ProjectCapability
Для проекта выше вы получите и TestingPlatformServer, и TestContainer. Это проект, настроенный на серверный режим MTP и одновременно тянущий за собой полный стек VSTest: в выходной папке лежит Microsoft.Testing.Platform.dll рядом с Microsoft.VisualStudio.TestPlatform.Common.dll, testhost.exe и xunit.runner.visualstudio.testadapter.dll. Две тестовые платформы в одной папке bin допустимы, но это значит, что runner, который вы проверяете из командной строки, не обязательно тот, который выбирает IDE.
Исправление 1: сделать оба пути кода одинаковыми, это и есть настоящее решение
Достаточно задать одно свойство, и сгенерированная точка входа переворачивается:
<!-- Directory.Build.props, .NET SDK 10.0.201, xunit.v3 3.2.2 -->
<Project>
<PropertyGroup>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
</PropertyGroup>
</Project>
Пересоберите и прочитайте XunitAutoGeneratedEntryPoint.cs заново:
// 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 является путём по умолчанию для любого вызова, поэтому успешный локальный запуск действительно проверяет тот код, который использует обозреватель тестов. Заодно вы получаете командную строку MTP, где и находятся средства диагностики. --diagnostic относится к MTP, а не к xUnit, и до этого изменения exe отклоняет его сообщением 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
Этот файл .diag и нужно читать, когда серверный режим застревает. В нём записана последовательность запуска платформы и каждое сообщение JSON-RPC, так что видно, не подключился ли процесс вовсе, подключился и не получил testing/runTests, либо получил и заблокировался внутри теста.
Дополните это режимом MTP для dotnet test в global.json, чтобы командная строка перестала проходить через мост VSTest:
{
"test": { "runner": "Microsoft.Testing.Platform" }
}
В режиме MTP свойство TestingPlatformDotnetTestSupport больше не нужно, и аргументам MTP больше не требуется дополнительный разделитель --.
Исправление 2: отключить возможность и вернуться к VSTest
Когда рабочий обозреватель тестов нужен немедленно, вместо этого уберите возможность:
<!-- Directory.Build.props. Restart Visual Studio after changing this. -->
<PropertyGroup>
<DisableTestingPlatformServerCapability>true</DisableTestingPlatformServerCapability>
</PropertyGroup>
Это документированный обходной путь от xUnit, и он делает ровно то, что написано в таргетах: TestingPlatformServer исчезает из списка возможностей, TestContainer сохраняется, поскольку его добавляет также Microsoft.NET.Test.Sdk, и Visual Studio возвращается к обнаружению тестов через xunit.runner.visualstudio. Проверяйте, а не предполагайте:
> dotnet msbuild -p:DisableTestingPlatformServerCapability=true -getItem:ProjectCapability
Перезапуск обязателен. Возможности проекта считываются при его загрузке, поэтому одна лишь пересборка оставит запущенную IDE со старым решением.
Исправление 3: удалите написанный вручную Main
Эту причину стоит проверять раньше остальных, потому что она даёт в точности описанную картину: командная строка зелёная, обозреватель тестов мёртв. Добавление Program.cs с инструкциями верхнего уровня в проект xUnit v3 заменяет не часть сгенерированной точки входа, а её целиком, и компилятор лишь предупреждает:
warning CS7022: The entry point of the program is global code;
ignoring 'XunitAutoGeneratedEntryPoint.Main(string[])' entry point.
Сборка с таким предупреждением всё равно успешна. Вот Program.cs, который выглядит вполне разумно и вручную поднимает консольный runner 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);
Запустите его без аргументов, и два теста пройдут. Запустите так, как это делает Visual Studio, и обработки --server просто не окажется:
> 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
Процесс пишет в stdout и завершается с кодом 3, так и не открыв сокет. Со стороны Visual Studio это runner, который ни к чему не подключился, что проявляется как ошибка подключения или как бесконечное ожидание. Если своя точка входа действительно нужна, задайте <GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint>, стройте её поверх TestApplication.CreateBuilderAsync(args) из Microsoft.Testing.Platform и передавайте args без изменений. Проглатывание массива аргументов и есть ошибка.
Исправление 4: одна мажорная версия Microsoft.Testing.Platform на решение
xUnit публикует варианты пакетов, закрепляющие разные мажорные версии MTP, и их смешение в одном решении порождает картину “часть проектов запускается, часть случайно нет” при запуске по всему решению. Измеренные разрешения зависимостей для xunit.v3 3.2.2:
| Ссылка на пакет | Разрешённая Microsoft.Testing.Platform |
|---|---|
xunit.v3 | 1.9.1 |
xunit.v3.mtp-v2 | 2.0.2 |
xunit.v3.mtp-off | нет, только VSTest |
Выберите один вариант и задайте его один раз в Directory.Build.props, а не в каждом проекте. Собственные рекомендации Microsoft по TestingPlatformDotnetTestSupport говорят то же самое про другое свойство: решение, где одни проекты используют одну платформу, а другие вторую, “может работать некорректно и является неподдерживаемым сценарием”. Новый тестовый проект из шаблона и есть обычный способ, которым рассогласование проникает внутрь.
Почему зелёный dotnet test доказывает меньше, чем кажется
Есть вариант и похуже: dotnet test может завершиться с кодом 0, не выполнив вообще ничего. Возьмите проект только с MTP, без Microsoft.NET.Test.Sdk, без xunit.runner.visualstudio и без секции test в global.json. dotnet test по умолчанию использует режим VSTest, не находит ни одного адаптера VSTest и рапортует об успехе:
# .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
Тестов два. Выполнилось ноль. Код возврата 0. Добавьте секцию runner в global.json из исправления 1, и та же команда сообщит total: 2, succeeded: 2. Если ваш CI сейчас зелёный на проекте xUnit v3, проверьте счётчик тестов в журнале, прежде чем ему доверять. Дешёвая страховка это --minimum-expected-tests через TestingPlatformCommandLineArguments:
<PropertyGroup>
<TestingPlatformCommandLineArguments>--minimum-expected-tests 1</TestingPlatformCommandLineArguments>
</PropertyGroup>
Похожие случаи, которые не являются этой ошибкой
Не всякий застрявший запуск в обозревателе тестов связан с серверным режимом. Эти варианты проверяйте после четырёх исправлений выше:
- Тест, который действительно уходит в взаимную блокировку. Если обнаружение завершается, а счётчик замирает посреди запуска всегда на одном и том же тесте, у вас блокирующий
.Resultили.Wait()на асинхронном вызове, а не проблема транспорта. Журнал.diagпокажет, чтоtesting/runTestsпришёл. DisableTestingPlatformServerCapabilityв проекте только с MTP. Измерено: при отсутствии пакетов VSTest это свойство убирает иTestingPlatformServer, иTestContainer, оставляя лишь две подвозможностиTestingPlatformServer.ExitOnProcessExitCapabilityиTestingPlatformServer.UseListTestsOptionForDiscoveryCapability. Проект после этого полностью исчезает из обозревателя тестов вместо отката к запасному варианту. Исправление 2 работает, только покаMicrosoft.NET.Test.Sdkостаётся в ссылках.- Выбранный в обозревателе тестов файл
.runsettings. У серверного режима и файлов параметров запуска есть собственные проблемы взаимодействия; снимите выбор в меню Тест > Настроить параметры запуска, прежде чем винить платформу. - Отсутствующие транзитивные сборки. Testhost, умирающий при запуске из-за того, что сборка из
deps.jsonне найдена, выглядит из IDE точно так же. Журнал приложений Windows и файл.diagразделяют эти два случая за секунды. - VS Code. C# Dev Kit читает ту же возможность
TestingPlatformServer, поэтому применимы те же четыре исправления, но его журналы находятся в канале вывода C# Dev Kit, а не вTestResults.
Связанные материалы
Если вы ещё выбираете фреймворк для стандартизации, измеренное сравнение xUnit v3, NUnit и MSTest разбирает то же разделение пакетов между MTP и VSTest со стороны выбора фреймворка. Когда набор тестов заработает стабильно, отчётность MTP 2.3 для GitHub Actions превратит падения в аннотации прямо в диффе pull request. Для уровня интеграционных тестов есть разбор WebApplicationFactory в ASP.NET Core 11 и сравнение WebApplicationFactory с Testcontainers. Если ваш CI сломался вокруг тестового инструментария по другой причине, удаление Newtonsoft.Json из VSTest это второе изменение, которое зацепило многих в этом году.
Источники
- Microsoft Testing Platform, документация xUnit.net v3 про
UseMicrosoftTestingPlatformRunner,DisableTestingPlatformServerCapabilityи варианты пакетовmtp-v1/mtp-v2/mtp-off. - Тестирование с помощью dotnet test, Microsoft Learn про режим VSTest против режима MTP,
TestingPlatformDotnetTestSupport, секцию runner вglobal.jsonиTestingPlatformCommandLineArguments. - xunit/xunit issue 3519, решение из 34 проектов на Visual Studio 18.4.0, где часть тестовых хостов так и не получает запрос
testing/runTests. - microsoft/testfx issue 4729 про “Unable to connect to testing platform runner process” в серверном режиме.
Microsoft.Testing.Platform.targetsиз пакетаmicrosoft.testing.platform2.3.3, где объявляется возможностьTestingPlatformServer.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.