You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 04:35:58