C#.NET6+Xunit单元测试失败时如何自定义格式化异常输出
方案生效前提说明
Xunit 测试执行器会主动捕获测试运行过程中所有抛出的异常,不会让异常流入AppDomain.UnhandledException管道,因此业务主程序的原有方案在测试场景下天然不生效,可通过以下两种可行方案实现需求:
方案1:自定义Fluent Assertions异常格式化器(推荐,适配FA断言场景)
该方案仅需注册一次,即可对所有Fluent Assertions断言抛出的异常自动生效,自定义输出格式。
- 实现自定义格式化器
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); } } }
- 全局注册格式化器
.NET 6+可直接使用模块初始化器实现全局注册,在测试项目中添加如下类即可:
using System.Runtime.CompilerServices; using FluentAssertions.Formatting; public static class TestInitializer { [ModuleInitializer] public static void InitFluentAssertionsFormatter() { Formatter.AddFormatter(new ExceptionWithDataFormatter()); } }
方案2:通用测试执行包装(适配所有异常场景)
该方案对测试代码中所有抛出的异常都生效,包括业务代码主动抛出的异常、第三方库抛出的异常等。
- 实现测试基类
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); } } }
- 测试类使用方式
所有测试类继承该基类,测试逻辑包裹在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
相关产品推荐
相关产品推荐

