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

C#.NET6+Xunit单元测试失败时如何自定义格式化异常输出

方案生效前提说明

Xunit 测试执行器会主动捕获测试运行过程中所有抛出的异常,不会让异常流入AppDomain.UnhandledException管道,因此业务主程序的原有方案在测试场景下天然不生效,可通过以下两种可行方案实现需求:


方案1:自定义Fluent Assertions异常格式化器(推荐,适配FA断言场景)

该方案仅需注册一次,即可对所有Fluent Assertions断言抛出的异常自动生效,自定义输出格式。

  1. 实现自定义格式化器
using FluentAssertions.Formatting;

public class ExceptionWithDataFormatter : IValueFormatter
{
    public bool CanHandle(object value) => value is Exception;

    public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild)
    {
        var ex = (Exception)value;
        formattedGraph.AddLine($"{ex.GetType().FullName}: {ex.Message}");
        
        // 自定义输出Data属性
        if (ex.Data.Count > 0)
        {
            formattedGraph.AddLine("=== Data 属性内容 ===");
            foreach (var key in ex.Data.Keys)
            {
                formattedGraph.AddLine($"  [{key}] = {ex.Data[key]}");
            }
        }

        // 保留原有堆栈、内部异常输出
        if (!string.IsNullOrEmpty(ex.StackTrace))
        {
            formattedGraph.AddLine("=== 堆栈跟踪 ===");
            formattedGraph.AddLine(ex.StackTrace);
        }

        if (ex.InnerException != null)
        {
            formattedGraph.AddLine("=== 内部异常 ===");
            formatChild(nameof(ex.InnerException), ex.InnerException, formattedGraph);
        }
    }
}
  1. 全局注册格式化器
    .NET 6+可直接使用模块初始化器实现全局注册,在测试项目中添加如下类即可:
using System.Runtime.CompilerServices;
using FluentAssertions.Formatting;

public static class TestInitializer
{
    [ModuleInitializer]
    public static void InitFluentAssertionsFormatter()
    {
        Formatter.AddFormatter(new ExceptionWithDataFormatter());
    }
}

方案2:通用测试执行包装(适配所有异常场景)

该方案对测试代码中所有抛出的异常都生效,包括业务代码主动抛出的异常、第三方库抛出的异常等。

  1. 实现测试基类
using Xunit.Abstractions;

public abstract class TestBase
{
    private readonly ITestOutputHelper _output;

    protected TestBase(ITestOutputHelper output)
    {
        _output = output;
    }

    // 同步测试用方法
    protected void RunTest(Action testLogic)
    {
        try
        {
            testLogic();
        }
        catch (Exception ex)
        {
            OutputCustomException(ex);
            throw;
        }
    }

    // 异步测试用方法
    protected async Task RunTestAsync(Func<Task> testLogic)
    {
        try
        {
            await testLogic();
        }
        catch (Exception ex)
        {
            OutputCustomException(ex);
            throw;
        }
    }

    private void OutputCustomException(Exception ex)
    {
        _output.WriteLine($"异常类型:{ex.GetType().FullName}");
        _output.WriteLine($"异常消息:{ex.Message}");
        
        if (ex.Data.Count > 0)
        {
            _output.WriteLine("Data 属性内容:");
            foreach (var key in ex.Data.Keys)
            {
                _output.WriteLine($"  {key} = {ex.Data[key]}");
            }
        }

        _output.WriteLine($"堆栈跟踪:{ex.StackTrace}");
        
        if (ex.InnerException != null)
        {
            _output.WriteLine("--- 内部异常 ---");
            OutputCustomException(ex.InnerException);
        }
    }
}
  1. 测试类使用方式
    所有测试类继承该基类,测试逻辑包裹在RunTest/RunTestAsync方法内即可:
public class SampleTest : TestBase
{
    public SampleTest(ITestOutputHelper output) : base(output) { }

    [Fact]
    public void TestSyncLogic()
    {
        RunTest(() =>
        {
            // 你的测试逻辑
            var ex = new InvalidOperationException("操作失败");
            ex.Data.Add("错误码", 1001);
            ex.Data.Add("请求ID", Guid.NewGuid());
            throw ex;
        });
    }

    [Fact]
    public async Task TestAsyncLogic()
    {
        await RunTestAsync(async () =>
        {
            // 你的异步测试逻辑
            await Task.Delay(10);
            throw new IOException("IO操作失败");
        });
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 16:54:03