如何让Visual Studio Test Explorer同步命令行/程序启动的测试结果
问题
我在一个包含约200个测试用例的.NET测试项目上卡了好几天,Visual Studio里直接运行测试一切正常,但想通过自定义CRM程序启动测试时遇到了两个问题:
- 尝试DTE/Marshal调用方式时,出现大量无效类错误,无法正常工作
- 改用
Process.Start执行命令行测试(vstest.console.exe或dotnet test),测试能正常运行,但Visual Studio的Test Explorer完全不同步,所有测试始终显示为未运行的蓝色状态
我试过的命令行命令:
- 使用vstest.console.exe:
"C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\Extensions\TestPlatform\VSTest.Console.exe" "MY_TEST_PROJECT\bin\Debug\net6.0\WebTests.dll" /TestAdapterPath:"MY_PACKAGES_PATH\packages" /Logger:console;verbosity=detailed
测试正常执行,cmd能看到进度,但Test Explorer无变化。
- 使用dotnet test:
dotnet test MY_TEST_PROJECT\WebTests.csproj
同样测试正常,但UI不更新。
此外还尝试过:
- 重装NUnit3TestAdapter
- 清理并重建解决方案
- 删除ComponentMountCache
- 使用dotnet vstest命令
测试项目的csproj配置:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <IsPackable>false</IsPackable> <PlatformTarget>x64</PlatformTarget> </PropertyGroup> <ItemGroup> <PackageReference Include="DocumentFormat.OpenXml" Version="2.10.0" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.59" /> <PackageReference Include="iTextSharp" Version="5.5.13.2" /> <PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" /> <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="5.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit.Analyzers" Version="3.6.1"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="NUnit3TestAdapter" Version="4.6.0" /> <PackageReference Include="Selenium.WebDriver" Version="4.16.2" /> <PackageReference Include="Selenium.Support" Version="4.16.2" /> <PackageReference Include="SeleniumExtras.WaitHelpers" Version="1.0.2" /> <PackageReference Include="Spire.PDF" Version="10.3.16" /> <PackageReference Include="Microsoft.TestPlatform" Version="17.8.0" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..MY_LIBRARY\MY_LIBRARY.csproj" /> </ItemGroup> <ItemGroup> <Reference Include="Microsoft.Web.Administration"> <HintPath>\\MY_ASSEMBLIES\Microsoft.Web.Administration.dll</HintPath> </Reference> <Reference Include="System.Management"> <HintPath>C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\System.Management.dll</HintPath> </Reference> </ItemGroup> <ItemGroup> <Compile Update="Properties\Resources.Designer.cs"> <DesignTime>True</DesignTime> <AutoGen>True</AutoGen> <DependentUpon>Resources.resx</DependentUpon> </Compile> </ItemGroup> <ItemGroup> <EmbeddedResource Update="Properties\Resources.resx"> <Generator>ResXFileCodeGenerator</Generator> <LastGenOutput>Resources.Designer.cs</LastGenOutput> </EmbeddedResource> </ItemGroup> </Project>
另外有一段DTE代码,理论上能触发测试,但它会随机获取当前运行的Visual Studio实例,而我经常同时打开多个VS窗口:
Sub RunAllTestsInVisualStudio() Dim dte As EnvDTE80.DTE2 = Nothing Try dte = DirectCast(Marshal.GetActiveObject("VisualStudio.DTE"), EnvDTE80.DTE2) If dte IsNot Nothing Then dte.ExecuteCommand("TestExplorer.ShowTestExplorer") dte.ExecuteCommand("Build.CleanSolution") Thread.Sleep(5000) dte.Solution.SolutionBuild.Build(True) 'ClearTestResults() dte.ExecuteCommand("TestExplorer.RunAllTests") End If Finally If dte IsNot Nothing Then Marshal.ReleaseComObject(dte) End Try End Sub
需要解决的核心问题:如何通过自定义程序启动测试,同时让Test Explorer同步更新测试结果?
解决方案
方案1:修复DTE调用,指定目标VS实例
原DTE代码的问题是Marshal.GetActiveObject随机返回VS实例,可通过WMI查询进程+解决方案路径定位目标实例:
Imports System.Management Imports EnvDTE80 Imports System.Runtime.InteropServices Sub RunTestsInTargetVSInstance(targetSolutionPath As String) Dim searcher As New ManagementObjectSearcher("root\CIMV2", "SELECT * FROM Win32_Process WHERE Name = 'devenv.exe'") For Each queryObj As ManagementObject In searcher.Get() Dim cmdLine As String = queryObj("CommandLine").ToString() ' 匹配加载目标解决方案的VS进程 If cmdLine.Contains(targetSolutionPath) Then Dim processId As Integer = Convert.ToInt32(queryObj("ProcessId")) Dim dte As DTE2 = GetDteByProcessId(processId) If dte IsNot Nothing Then dte.ExecuteCommand("TestExplorer.ShowTestExplorer") dte.ExecuteCommand("Build.CleanSolution") ' 等待构建完成再启动测试,替代Thread.Sleep WaitForBuildCompletion(dte.Solution.SolutionBuild) dte.ExecuteCommand("TestExplorer.RunAllTests") Marshal.ReleaseComObject(dte) Exit For End If End If Next End Sub Private Function GetDteByProcessId(processId As Integer) As DTE2 Dim dte As DTE2 = Nothing Dim rot As IRunningObjectTable = Nothing Dim enumMoniker As IEnumMoniker = Nothing Dim monikers(0) As IMoniker Try GetRunningObjectTable(0, rot) rot.EnumRunning(enumMoniker) enumMoniker.Reset() While enumMoniker.Next(1, monikers, Nothing) = 0 Dim name As String = "" monikers(0).GetDisplayName(Nothing, Nothing, name) ' 匹配格式:!VisualStudio.DTE.17.0:进程ID If name.StartsWith("!VisualStudio.DTE") AndAlso name.EndsWith($":{processId}") Then Dim obj As Object = Nothing rot.GetObject(monikers(0), obj) dte = DirectCast(obj, DTE2) Exit While End If End While Catch ex As Exception ' 可添加日志记录逻辑 Finally ' 释放COM资源 If enumMoniker IsNot Nothing Then Marshal.ReleaseComObject(enumMoniker) If rot IsNot Nothing Then Marshal.ReleaseComObject(rot) If monikers(0) IsNot Nothing Then Marshal.ReleaseComObject(monikers(0)) End Try Return dte End Function Private Sub WaitForBuildCompletion(solutionBuild As SolutionBuild) solutionBuild.Build(True) While solutionBuild.BuildState = vsBuildState.vsBuildStateInProgress System.Threading.Thread.Sleep(500) End While End Sub ' 导入所需COM接口 <ComImport, Guid("00000010-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> Private Interface IRunningObjectTable Sub _VtblGap1_6() Function EnumRunning(<Out> ByRef ppenumMoniker As IEnumMoniker) As Integer Function GetObject(ByVal pmkObjectName As IMoniker, <Out> ByRef ppunkObject As Object) As Integer End Interface <ComImport, Guid("0000000E-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> Private Interface IEnumMoniker Function Next(ByVal celt As Integer, <Out> ByRef rgelt As IMoniker, <Out> ByRef pceltFetched As Integer) As Integer Sub Skip(ByVal celt As Integer) Sub Reset() Function Clone(<Out> ByRef ppenum As IEnumMoniker) As Integer End Interface <DllImport("ole32.dll")> Private Shared Function GetRunningObjectTable(ByVal reserved As Integer, <Out> ByRef prot As IRunningObjectTable) As Integer End Function
方案2:让命令行测试结果同步到Test Explorer
命令行测试无法同步是因为Test Explorer只监听内部会话,可通过生成trx结果文件+DTE导入实现同步:
- 修改vstest.console.exe命令,添加trx日志参数:
"C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\Extensions\TestPlatform\VSTest.Console.exe" "MY_TEST_PROJECT\bin\Debug\net6.0\WebTests.dll" /TestAdapterPath:"MY_PACKAGES_PATH\packages" /Logger:trx;LogFileName=TestResults.trx
- 测试完成后,通过DTE命令导入结果:
' 假设已获取目标DTE实例 dte.ExecuteCommand("TestExplorer.ImportTestResults", "PATH_TO\TestResults.trx")
方案3:使用Test Platform编程接口直接启动测试
直接引用Test Platform的客户端包,在自定义程序中启动测试并同步结果:
- 添加NuGet包引用:
<PackageReference Include="Microsoft.TestPlatform.ObjectModel" Version="17.8.0" /> <PackageReference Include="Microsoft.TestPlatform.Client" Version="17.8.0" />
- 编写测试启动代码:
Imports Microsoft.VisualStudio.TestPlatform.Client Imports Microsoft.VisualStudio.TestPlatform.ObjectModel Imports Microsoft.VisualStudio.TestPlatform.ObjectModel.Client Sub RunTestsWithTestPlatform(testDllPath As String, adapterPath As String, dte As DTE2) Dim testPlatformClient As ITestPlatform = TestPlatformFactory.GetTestPlatform() Dim runSettings As String = "<RunSettings><RunConfiguration><TargetPlatform>x64</TargetPlatform></RunConfiguration></RunSettings>" Dim testRunRequest As ITestRunRequest = testPlatformClient.CreateTestRunRequest( New List(Of String) From {testDllPath}, runSettings, New List(Of String) From {adapterPath} ) ' 监听测试进度 AddHandler testRunRequest.TestRunChanged, Sub(sender, args) If args.TestRunStatistics IsNot Nothing Then Console.WriteLine($"已完成 {args.TestRunStatistics.ExecutedTests} 个测试") End If End Sub ' 启动测试并在完成后导入结果到Test Explorer testRunRequest.OnTestRunComplete += Sub(sender, args) If args.TestRunResult IsNot Nothing AndAlso args.TestRunResult.Attachments.Any() Then Dim trxPath = args.TestRunResult.Attachments.First(Function(a) a.Uri.AbsolutePath.EndsWith(".trx")).Uri.AbsolutePath dte.ExecuteCommand("TestExplorer.ImportTestResults", trxPath) End If End Sub testRunRequest.ExecuteAsync() End Sub
内容的提问来源于stack exchange,提问作者Yisroel M. Olewski
相关产品推荐
相关产品推荐

