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

如何为自定义属性与测试数据的每种组合执行Xunit测试

问题需求与现状

需要实现自定义User属性与每组测试数据的全组合测试,预期生成4个测试用例:

  • [User("A")] 搭配 ("test1", 1)
  • [User("A")] 搭配 ("test2", 2)
  • [User("B")] 搭配 ("test1", 1)
  • [User("B")] 搭配 ("test2", 2)

要求在测试的Initialize阶段获取User属性值,但当前测试方法未直接使用该属性。

现有问题

  1. 直接使用[Theory]+[InlineData]+多个[User]属性时,虽能生成4个测试用例,但Xunit会因测试输入数据重复(如("test1",1)出现两次)自动移除重复用例,导致实际运行的测试数量不足。
  2. 尝试自定义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无法被识别的核心原因:

  1. XunitTestCaseDiscoverer的程序集名称配置错误,[XunitTestCaseDiscoverer("xxx", "xxx")]中的第一个参数是Discoverer的完整类型名(命名空间+类名),第二个是程序集名称,必须准确匹配,否则Xunit找不到Discoverer。
  2. IAttributeInfo是Xunit内部接口,未实现序列化逻辑,直接存储会导致序列化失败,测试用例无法被持久化和识别。
  3. 自定义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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 11:22:53