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

Xunit测试结果附件无法显示:Azure DevOps上传问题求助

问题排查与修复方案

针对你遇到的Azure DevOps测试结果附件无法显示问题,结合代码分析,以下是具体修复步骤:


1. 核心问题定位

错误点1:绑定到错误的测试结果

当前代码直接取测试运行中的第一个结果(testResults.FirstOrDefault()),而非生成日志的具体测试用例结果,导致附件可能被绑定到其他测试,你在目标测试结果中自然看不到。

错误点2:依赖"最新运行ID"存在歧义

通过API获取最新运行ID可能会获取到其他并行执行的测试运行,而非当前正在执行的测试运行。

错误点3:动态反序列化易引发属性映射错误

使用dynamic类型反序列化API响应,可能因大小写或属性名不匹配导致测试结果ID获取失败。


2. 具体修复代码

修复1:获取当前测试用例名称(Xunit环境)

在测试类中注入ITestOutputHelper,获取当前执行的测试用例名称:

public class YourTestClass
{
    private readonly ITestOutputHelper _output;
    private string _currentTestName;

    public YourTestClass(ITestOutputHelper output)
    {
        _output = output;
        // 获取当前测试用例的显示名称
        _currentTestName = ((TestOutputHelper)output).Test.DisplayName;
    }

    [Fact]
    public async Task CreateTnMProjectTest()
    {
        // 测试逻辑...
        
        // 上传日志时传入当前测试用例名称
        await UploadLogForCurrentTest();
    }

    private async Task UploadLogForCurrentTest()
    {
        try
        {
            string pat = Environment.GetEnvironmentVariable("AZURE_DEVOPS_PAT");
            // 直接使用流水线提供的当前测试运行ID(替代GetLatestRunId)
            string runId = Environment.GetEnvironmentVariable("TEST_RUNID");
            
            // 根据测试用例名称获取对应的结果ID
            var testResults = await devOpsUploader.FetchTestResultsByTestCaseName(runId, _currentTestName);
            string testCaseResultId = testResults.FirstOrDefault()?.Id.ToString() ?? "";
            
            if(string.IsNullOrEmpty(testCaseResultId))
            {
                Console.WriteLine("未找到当前测试用例对应的结果ID");
                return;
            }

            string comment = "Log file attachment";
            string basePath = @"D:\a\1\s\TestProject1\bin\Debug\netcoreapp3.1\Logs";
            string fileName = $"CreateTnMProjectTest_{DateTime.Now:yyyyMMdd_HHmmss}.txt";
            string fullPath = Path.Combine(basePath, fileName);
           
            await devOpsUploader.UploadLogFile(runId, testCaseResultId, fullPath, comment);
            
            // 验证上传结果
            var attachments = await devOpsUploader.GetTestResultAttachments(runId, testCaseResultId);
            var uploadedAttachment = attachments.FirstOrDefault(a => a.FileName == fileName);
            if(uploadedAttachment != null)
            {
                Console.WriteLine($"附件已成功上传,ID:{uploadedAttachment.Id}");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"上传日志时出错:{ex.Message}");
        }
    }
}

修复2:强类型反序列化并过滤测试结果

替换原FetchTestResults方法,使用强类型类反序列化API响应,并根据测试用例名称过滤:

// 新增强类型辅助类
public class TestResultsResponse
{
    [JsonProperty("value")]
    public List<TestResultDto> Results { get; set; }
}

public class TestResultDto
{
    [JsonProperty("id")]
    public int Id { get; set; }

    [JsonProperty("outcome")]
    public string Outcome { get; set; }

    [JsonProperty("testCase")]
    public TestCaseDto TestCase { get; set; }
}

public class TestCaseDto
{
    [JsonProperty("name")]
    public string Name { get; set; }
}

// 替换原FetchTestResults方法
public async Task<List<TestResult>> FetchTestResultsByTestCaseName(string runId, string testCaseName)
{
    string organization = "********";
    string project = "Training";
    string apiUrl = $"https://dev.azure.com/{organization}/{project}/_apis/test/Runs/{runId}/Results?api-version=6.0-preview.1";

    using (HttpClient httpClient = new HttpClient())
    {
        string pat = Environment.GetEnvironmentVariable("AZURE_DEVOPS_PAT");
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($":{pat}")));

        HttpResponseMessage response = await httpClient.GetAsync(apiUrl);
        response.EnsureSuccessStatusCode();

        var testResultData = await response.Content.ReadAsStringAsync();
        var testResultsResponse = JsonConvert.DeserializeObject<TestResultsResponse>(testResultData);

        List<TestResult> resultList = new List<TestResult>();

        foreach (var result in testResultsResponse.Results)
        {
            resultList.Add(new TestResult(new TestCase { Name = result.TestCase.Name })
            {
                Id = result.Id,
                Outcome = result.Outcome,
                RunId = runId
            });
        }

        // 过滤出当前测试用例的结果
        return resultList.Where(r => r.TestCase.Name.Equals(testCaseName, StringComparison.OrdinalIgnoreCase)).ToList();
    }
}

修复3:新增附件验证方法

添加方法验证附件是否成功上传:

public class TestAttachmentsResponse
{
    [JsonProperty("value")]
    public List<TestAttachment> Attachments { get; set; }
}

public class TestAttachment
{
    [JsonProperty("id")]
    public int Id { get; set; }

    [JsonProperty("fileName")]
    public string FileName { get; set; }

    [JsonProperty("comment")]
    public string Comment { get; set; }
}

public async Task<List<TestAttachment>> GetTestResultAttachments(string runId, string testCaseResultId)
{
    string organization = "********";
    string project = "Training";
    string apiUrl = $"https://dev.azure.com/{organization}/{project}/_apis/test/Runs/{runId}/Results/{testCaseResultId}/attachments?api-version=6.0-preview.1";

    using (HttpClient httpClient = new HttpClient())
    {
        string pat = Environment.GetEnvironmentVariable("AZURE_DEVOPS_PAT");
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($":{pat}")));

        HttpResponseMessage response = await httpClient.GetAsync(apiUrl);
        response.EnsureSuccessStatusCode();

        var attachmentsResponse = await response.Content.ReadAsStringAsync();
        var attachments = JsonConvert.DeserializeObject<TestAttachmentsResponse>(attachmentsResponse);

        return attachments.Attachments;
    }
}

修复4:流水线配置调整

在Azure DevOps流水线的Visual Studio测试任务中,启用"发布测试结果到Azure DevOps",系统会自动设置Test.RunId环境变量(转换为大写下划线格式TEST_RUNID)。


3. 额外注意事项

  • PAT权限:确保PAT拥有**测试管理(读写)**权限。
  • 测试运行状态:附件需等待整个测试运行完成后才会在Azure DevOps UI中显示,不要在运行过程中检查。
  • 文件路径:确认流水线中日志文件生成路径与代码中basePath一致。

内容的提问来源于stack exchange,提问作者Pavan Peshve

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 17:22:07