如何为自定义属性与测试数据的每种组合执行Xunit测试
问题需求与现状
需要实现自定义User属性与每组测试数据的全组合测试,预期生成4个测试用例:
[User("A")]搭配("test1", 1)[User("A")]搭配("test2", 2)[User("B")]搭配("test1", 1)[User("B")]搭配("test2", 2)
要求在测试的Initialize阶段获取User属性值,但当前测试方法未直接使用该属性。
现有问题
- 直接使用
[Theory]+[InlineData]+多个[User]属性时,虽能生成4个测试用例,但Xunit会因测试输入数据重复(如("test1",1)出现两次)自动移除重复用例,导致实际运行的测试数量不足。 - 尝试自定义
TestCase承载UserAttribute后,Xunit构建完成后无法识别任何测试用例。
初始实现代码
public class ExapmleTest { [User("A")] [User("B")] [Theory] [InlineData("test1", 1)] [InlineData("test2", 2)] public void ExapmleTest1(string testInput, int count) { // Test body } }
自定义Attribute与Discoverer尝试代码
public class UserAttribute : Attribute { public UserAttribute(string name) { Name = name; } public string Name { get; } } [XunitTestCaseDiscoverer("xxx", "xxx")] // 需要替换为实际程序集名称 public class MyTheoryAttribute : TheoryAttribute { } public class TestCaseTheoryDiscoverer : IXunitTestCaseDiscoverer { readonly TheoryDiscoverer theoryDiscoverer; private readonly IMessageSink _diagnosticMessageSink; public IMessageSink DiagnosticMessageSink => _diagnosticMessageSink; public TestCaseTheoryDiscoverer(IMessageSink diagnosticMessageSink) { theoryDiscoverer = new TheoryDiscoverer(diagnosticMessageSink); _diagnosticMessageSink = diagnosticMessageSink; } public IEnumerable<IXunitTestCase> Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) { var userAttributes = testMethod.Method.GetCustomAttributes(typeof(UserAttribute)).ToList(); var inlineDataAttributes = testMethod.Method.GetCustomAttributes(typeof(InlineDataAttribute)).ToList(); foreach (var userAttribute in userAttributes) { foreach (var inlineDataAttribute in inlineDataAttributes) { var userAttributeData = userAttribute.GetConstructorArguments().FirstOrDefault(); var constructorArgsArray = inlineDataAttribute.GetConstructorArguments().ToArray(); var constructorArgs = (object[])constructorArgsArray[0]; var testCase = new MyTestCase(_diagnosticMessageSink, TestMethodDisplay.ClassAndMethod, TestMethodDisplayOptions.None, testMethod, userAttribute, constructorArgs.Cast<object>().ToArray()); yield return testCase; } } }
自定义TestCase代码
public class MyTestCase : XunitTestCase { public IAttributeInfo _userAttribute; public MyTestCase() { } public MyTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, IAttributeInfo userAttribute, object[] testMethodArguments) : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) { _userAttribute = userAttribute; } public override void Serialize(IXunitSerializationInfo data) { base.Serialize(data); data.AddValue("UserAttribute", _userAttribute); } public override void Deserialize(IXunitSerializationInfo data) { base.Deserialize(data); _userAttribute = data.GetValue<IAttributeInfo>("UserAttribute"); } protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) { var userAttributeValue = _userAttribute.GetConstructorArguments().First(); return $"{base.GetDisplayName(factAttribute, displayName)} ({userAttributeValue})"; } }
测试类代码(使用自定义Attribute)
public class ExapmleTest { [User("A")] [User("B")] [MyTheory] [InlineData("test1", 1)] [InlineData("test2", 2)] public void ExapmleTest1(string testInput, int count) { // Test body } }
问题分析
自定义TestCase无法被识别的核心原因:
XunitTestCaseDiscoverer的程序集名称配置错误,[XunitTestCaseDiscoverer("xxx", "xxx")]中的第一个参数是Discoverer的完整类型名(命名空间+类名),第二个是程序集名称,必须准确匹配,否则Xunit找不到Discoverer。IAttributeInfo是Xunit内部接口,未实现序列化逻辑,直接存储会导致序列化失败,测试用例无法被持久化和识别。- 自定义TestCase的显示名称生成逻辑存在潜在问题,且未正确将User属性传递到测试执行上下文。
修正后的解决方案
步骤1:修正Discoverer的Attribute配置
确保XunitTestCaseDiscoverer参数与实际代码匹配,例如TestCaseTheoryDiscoverer在MyTestProject.XunitExtensions命名空间下,程序集名为MyTestProject:
[XunitTestCaseDiscoverer("MyTestProject.XunitExtensions.TestCaseTheoryDiscoverer", "MyTestProject")] public class MyTheoryAttribute : TheoryAttribute { }
步骤2:替换不可序列化的IAttributeInfo
提取UserAttribute的实际值(如Name)存储,因为string可序列化。修改MyTestCase:
public class MyTestCase : XunitTestCase { public string UserName { get; private set; } // 必须保留无参构造函数用于序列化 public MyTestCase() { } public MyTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, string userName, object[] testMethodArguments) : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) { UserName = userName; } public override void Serialize(IXunitSerializationInfo data) { base.Serialize(data); data.AddValue("UserName", UserName); } public override void Deserialize(IXunitSerializationInfo data) { base.Deserialize(data); UserName = data.GetValue<string>("UserName"); } protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) { return $"{base.GetDisplayName(factAttribute, displayName)} (User: {UserName})"; } // 重写ExecuteAsync,将UserName传递到测试上下文 public override async Task<RunSummary> ExecuteAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, object[] constructorArguments, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) { // 将UserName注入测试实例的属性中 var testInstance = constructorArguments[0]; var userProperty = testInstance.GetType().GetProperty("CurrentUserName"); if (userProperty != null) { userProperty.SetValue(testInstance, UserName); } return await base.ExecuteAsync(diagnosticMessageSink, messageBus, constructorArguments, aggregator, cancellationTokenSource); } }
步骤3:修正Discoverer的逻辑,提取User属性值
修改TestCaseTheoryDiscoverer的Discover方法,避免处理IAttributeInfo:
public IEnumerable<IXunitTestCase> Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) { var userNames = testMethod.Method.GetCustomAttributes(typeof(UserAttribute)) .Select(attr => attr.GetConstructorArguments().FirstOrDefault() as string) .Where(name => !string.IsNullOrEmpty(name)) .ToList(); var inlineDataAttributes = testMethod.Method.GetCustomAttributes(typeof(InlineDataAttribute)).ToList(); foreach (var userName in userNames) { foreach (var inlineDataAttribute in inlineDataAttributes) { var constructorArgs = inlineDataAttribute.GetConstructorArguments().Cast<object>().ToArray(); // 处理InlineData的参数格式 var testArgs = constructorArgs.Length == 1 && constructorArgs[0] is object[] array ? array : constructorArgs; var testCase = new MyTestCase(_diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, userName, testArgs); yield return testCase; } } }
步骤4:修改测试类,添加获取UserName的属性
public class ExapmleTest { public string CurrentUserName { get; set; } [User("A")] [User("B")] [MyTheory] [InlineData("test1", 1)] [InlineData("test2", 2)] public void ExapmleTest1(string testInput, int count) { // 可在测试逻辑或Initialize方法中使用CurrentUserName Console.WriteLine($"当前用户:{CurrentUserName},测试输入:{testInput},计数:{count}"); // Test body } }
步骤5:确保Xunit版本兼容
使用Xunit 2.4+版本,该版本的自定义TestCase和DiscovererAPI较为稳定,避免版本差异导致的问题。
内容的提问来源于stack exchange,提问作者Aleksandra Kukowka
相关产品推荐
相关产品推荐

