如何实现TeamCity、XUnit与Coverlet的协同工作?
.NET + xUnit + TeamCity 多测试项目覆盖率配置记录
尝试多种无效方案后,耗时两天才完成配置,特此记录供后续参考,也希望能帮助到其他开发者。我的配置基于一篇技术文章,但原文中可用的Coverlet.MsBuild在我这边无论如何都无法生成结果,因此不得不改用Coverlet.Collector,这也让配置过程复杂了不少。以下是我配置PowerShell脚本为TeamCity生成覆盖率报告的具体步骤:
#Error Action Preference: # # PowerShell halts execution on terminating errors, as mentioned before. # For non-terminating errors we have the option to tell PowerShell how # to handle these situations. # This is where the error action preference comes in. Error Action # Preference allows us to specify the desired behavior for a non-terminating # error; it can be scoped at the command level or all the way up to the # script level. # #Available choices for error action preference: # # SilentlyContinue – error messages are suppressed and execution continues. # Stop – forces execution to stop, behaving like a terminating error. # Continue - the default option. Errors will display and execution will continue. # Inquire – prompt the user for input to see if we should proceed. # Ignore – (new in v3) – the error is ignored and not logged to the error stream. Has very restricted usage scenarios. # Example: Set the preference at the script scope to Stop, place the following near the top of the script file: $ErrorActionPreference = "Stop" Set-StrictMode -Version 3.0 # Turns all sorts of silent errors into true errors # This file is used by TeamCity build steps $checkoutDir = $args[0] Write-Output "`$checkoutDir : $checkoutDir" $donetCliExePath = $args[1] Write-Output "`$donetCliExePath : $donetCliExePath" $coverletPath = "$checkoutDir\coverlet" Write-Output "`$coverletPath : $coverletPath" $coverageOutputPath = "$checkoutDir\coverage" Write-Output "`$coverageOutputPath: $coverageOutputPath" # The TeamCity Build Agent runs using the LocalSystem account # and all user profile paths are defined in an Environment # variable $env:USERPROFILE. All global dotnet tools are installed # in the running users (LocalSystem) profile in the Path below: $dotNetToolPath = "$env:USERPROFILE\.dotnet\tools" Write-Output "`$dotNetToolPath : $dotNetToolPath" Set-Alias -Name DotNet -Value "$donetCliExePath" try { Write-output "Ensuring reportgenerator is available." & DotNet tool update --global dotnet-reportgenerator-globaltool # Get a list of all unit testing projects $testCsProjList = Get-Childitem -Recurse $checkoutDir -Filter *.Tests.Unit.csproj # Set up a job where each unit test dll is run in parallel # up to 10 test fixtures at a time. Write-Output "" Write-output "Running unit tests and collecting code coverage..." Write-Output "" $testCsProjList | foreach-Object -ThrottleLimit 10 -Parallel { DotNet test ` "$($_.FullName)" ` --no-build ` -c Release ` -l "console;verbosity=normal" ` --test-adapter-path "$env:USERPROFILE\.nuget\packages\xunit.runner.visualstudio\2.4.5\build\netcoreapp3.1" ` --collect:"XPlat Code Coverage" 2>&1 } Write-Output "" Write-Output "-----------------------------COMPLETE----------------------------------" Write-Output "" # Coverlet.Collector puts the coverage results in a dynamic folder. # Coverlet.MsBuild存在已知问题,该问题只能通过切换到Coverlet.Collector解决 # 虽然我们没遇到这个错误,但为了规避潜在的覆盖率中断问题,选择使用Coverlet.Collector # 当前无法指定Coverlet.Collector的输出文件夹,只能在Tests目录中搜索它生成的cobertura.xml文件 $found = Get-ChildItem -Path "$checkoutDir\tests" -Filter *.cobertura.xml -Recurse -File | Select-Object -ExpandProperty FullName if ($null -eq $found) { throw "No Coverage Results, please view logs for details. Exiting." } if(!(Test-Path $coverageOutputPath)) { # Move-Item不会自动创建目标路径,因此先确保路径存在 New-Item -Path $coverageOutputPath -ItemType Directory -Force | Out-Null } if(!(Test-Path $coverletPath)) { New-Item -Path $coverletPath -ItemType Directory -Force | Out-Null } Write-output "Moving all coverage results:" $found | ForEach-Object { $projectCoveragePath = $_.Trim() if([string]::IsNullOrWhitespace($_)) { # 偶尔会匹配到空结果,直接跳过 continue } $foundTestNames = $projectCoveragePath | Select-String 'unit\\(.*)\\TestResults' -AllMatches if($foundTestNames.Matches.Count -ne 1) { throw "Could not find coverage results. Likely cause: Unit Testing folder structure." } $testProjectName = $foundTestNames.Matches.Groups[1].Value # 变量赋值提升可读性 $destinationPath = "$coverletPath\$testProjectName.cobertura.xml" Write-Output "Moving $testProjectName coverage results to: $destinationPath" Move-Item -Path "$projectCoveragePath" -Destination "$destinationPath" -Force } Write-output "Generating TeamCity Report:" & "$dotNetToolPath\reportgenerator.exe" "-targetdir:$coverageOutputPath" "-reports:$coverletPath\*.xml" "-reporttypes:Html" if($LastExitCode -eq 1) { throw "Running Unit Tests and Gathering coverage of Nexus.Api.SDK failed, please view logs for details. Exiting." } } catch { $Exception = $_; # 显示异常信息 $Exception | Format-List * -Force | Out-String ; # 显示导致错误的调用上下文信息 $Exception.InvocationInfo | Format-List * -Force | Out-String ; $Exception = $_.Exception; # 递归显示所有内部异常(深度优先) for ($i = 0; $Exception; $i++, ($Exception = $Exception.InnerException)) { Write-Output ("$i" * 80) ; $Exception | Format-List * -Force | Out-String } throw }
如果有更简便的实现方式,欢迎告知!
内容的提问来源于stack exchange,提问作者Bitfiddler
相关产品推荐
相关产品推荐

