能否在XUnit中统一包装测试执行?自定义异常日志优化问询
统一处理XUnit中自定义异常的日志记录
针对你遇到的问题——不想逐个修改测试来捕获MyException并记录SomeProperty,这里有几个可行的统一处理方案:
方案1:写个测试基类统一包裹逻辑
创建一个所有测试类都继承的基类,把异常捕获和日志记录的逻辑封装进去,后续测试只要调用基类的方法执行测试代码就行。
示例代码:
public class TestBase : IDisposable { protected readonly ITestOutputHelper Output; public TestBase(ITestOutputHelper output) { Output = output; } protected void RunTest(Action testAction) { try { testAction(); } catch (MyException ex) { // 把自定义属性打日志 Output.WriteLine($"MyException的SomeProperty:{ex.SomeProperty}"); // 重新抛出,不影响测试标记为失败 throw; } } public void Dispose() { // 有需要的话加清理逻辑 } }
测试类这么用:
public class UserTests : TestBase { public UserTests(ITestOutputHelper output) : base(output) { } [Fact] public void CreateUser_InvalidInput_ThrowsMyException() { RunTest(() => { // 你的测试逻辑,比如抛出MyException throw new MyException("参数非法") { SomeProperty = "InvalidUserName" }; }); } }
方案2:自定义Fact属性实现全局拦截
这个方案最彻底,不需要修改测试逻辑,只要把原来的[Fact]换成自定义的属性就行,适合大规模改造现有测试。
首先写自定义测试运行器,用来捕获异常:
public class MyExceptionLoggingTestCaseRunner : XunitTestCaseRunner { private readonly ITestOutputHelperAccessor _outputHelperAccessor; public MyExceptionLoggingTestCaseRunner(IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments, object[] testMethodArguments, IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestOutputHelperAccessor outputHelperAccessor) : base(testCase, displayName, skipReason, constructorArguments, testMethodArguments, diagnosticMessageSink, messageBus) { _outputHelperAccessor = outputHelperAccessor; } protected override async Task<RunSummary> RunTestAsync() { try { return await base.RunTestAsync(); } catch (MyException ex) { var outputHelper = _outputHelperAccessor.OutputHelper; outputHelper?.WriteLine($"捕获到MyException,SomeProperty:{ex.SomeProperty}"); throw; } } }
然后写测试发现器:
public class MyExceptionLoggingTestCaseDiscoverer : IXunitTestCaseDiscoverer { private readonly IMessageSink _diagnosticMessageSink; public MyExceptionLoggingTestCaseDiscoverer(IMessageSink diagnosticMessageSink) { _diagnosticMessageSink = diagnosticMessageSink; } public IEnumerable<IXunitTestCase> Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) { yield return new XunitTestCase(_diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod) { RunnerFactory = (testCase, displayName, skipReason, constructorArguments, testMethodArguments, messageBus) => new MyExceptionLoggingTestCaseRunner(testCase, displayName, skipReason, constructorArguments, testMethodArguments, _diagnosticMessageSink, messageBus, new TestOutputHelperAccessor()) }; } }
最后定义自定义Fact属性:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] [XunitTestCaseDiscoverer("你的命名空间.MyExceptionLoggingTestCaseDiscoverer", "你的程序集名称")] public class LoggedFactAttribute : FactAttribute { }
之后测试方法直接用[LoggedFact]替换[Fact]就行,不用改测试逻辑:
public class OrderTests { private readonly ITestOutputHelper _output; public OrderTests(ITestOutputHelper output) { _output = output; } [LoggedFact] public void SubmitOrder_StockNotEnough_ThrowsMyException() { // 测试逻辑,抛出MyException时会自动记录SomeProperty throw new MyException("库存不足") { SomeProperty = "Order-12345" }; } }
方案3:写个扩展方法快速包裹测试
如果不想改基类也不想自定义属性,写个扩展方法,在测试里一行代码包裹逻辑就行,改动量也很小。
扩展方法代码:
public static class TestOutputHelperExtensions { public static void LogMyException(this ITestOutputHelper output, Action testAction) { try { testAction(); } catch (MyException ex) { output.WriteLine($"MyException的SomeProperty:{ex.SomeProperty}"); throw; } } }
测试里这么用:
[Fact] public void UpdateProduct_InvalidPrice_ThrowsMyException() { _output.LogMyException(() => { // 测试逻辑 throw new MyException("价格非法") { SomeProperty = "Product-6789" }; }); }
选哪个?
- 新项目或者测试不多:方案1最省心,统一继承基类就行;
- 已有大量测试不想改逻辑:方案2最适合,换个属性就搞定;
- 只想快速改几个测试:方案3最灵活,不用动结构。
内容的提问来源于stack exchange,提问作者Ilya Chernomordik
相关产品推荐
相关产品推荐

