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

xUnit Assert.Equivalent测试协变返回类型抽象属性时抛ArgumentException

xUnit Assert.Equivalent 因继承属性重复键报错的无修改类结构解决方案

问题场景

在.NET 8项目中使用xUnit 2.6.3测试继承结构的对象时,调用Assert.Equivalent(expected, result, true)抛出System.ArgumentException,提示"已添加相同键的项。键: Data",异常来自AssertHelper.GetGettersForType方法。

具体结构:

  • 基类Widget包含抽象只读属性Data(类型WidgetData)
  • 派生类QueryCountWidget重写Data属性,返回QueryCountWidgetData(WidgetData的子类)
  • 调试确认返回对象与预期一致,但整体断言失败。

相关类定义

Widget 类

public abstract class Widget
{
    public virtual string Title { get; set; }
    public int RowSpan { get; set; }
    public int ColumnSpan { get; set; }
    public int Row { get; set; }
    public int Column { get; set; }
    public string BackgroundColor { get; set; } = "DarkGray";
    public int DefaultFontSize { get; set; }
    public abstract WidgetData Data { get; }
    public abstract void SetData(object data);
}

QueryWidget 类

public abstract class QueryWidget : Widget
{
    public Guid QueryId { get; set; }
    public List<SelectableQueryParameter> Parameters { get; set; }
    public DateSearchContext SearchContext { get; set; }
}

QueryCountWidget 类

public class QueryCountWidget : QueryWidget
{
    private QueryCountWidgetData _data;
    public override QueryCountWidgetData Data => _data;

    public override void SetData(object data)
    {
        if (data is QueryCountWidgetData widgetData)
        {
            _data = widgetData;
        }
    }
}

WidgetData 派生类

public abstract class WidgetData
{ }

public class QueryCountWidgetData : WidgetData
{
    public int? Count { get; set; }
}

测试代码

[Fact]
public void DeserializeQueryCountWidget()
{
    var result = DashboardXmlDeserializer.GetWidgetFromXmlNode(_queryCountWidgetNode);
    Assert.NotNull(result);
    Assert.IsType<QueryCountWidget>(result);
    var expected = new QueryCountWidget
    {
        Title = "QueryCount with Param",
        BackgroundColor = "DarkGray",
        Row = 0,
        RowSpan = 1,
        Column = 8,
        ColumnSpan = 1,
        SearchContext = new DateSearchContext
        {
            Name = "",
            DateFieldId = "",
            DateSearchShortcut = "All"
        },
        Parameters = new List<SelectableQueryParameter>
        {
            new SelectableQueryParameter
            {
                Name = "@TestParam",
                Value = "Scrap",
                DataTypeString = "System.String",
                Hidden = false,
            }
        },
        QueryId = Guid.Parse("f545ad33-647c-4596-ad7d-0b0a113e7e98"),
    };
    Assert.Equivalent(expected, result, true);
}

已尝试的无效/破坏性方案

  • 将Widget.Data改为virtual而非abstract:仍抛出相同异常
  • 使用new关键字覆盖Widget.Data:仍抛出相同异常
  • 移除Widget.Data仅保留派生类属性:测试成功,但破坏类结构
  • 修改Widget.Data类型为QueryCountWidgetData:测试成功,但无法适配其他Widget类型

无修改类结构的解决方案

方案1:转换为派生类类型后再断言

测试中已经通过Assert.IsType<QueryCountWidget>(result)确认对象类型,直接将结果转换为派生类后调用断言,xUnit会以派生类类型解析属性,避免基类与派生类属性重复:

[Fact]
public void DeserializeQueryCountWidget()
{
    var result = DashboardXmlDeserializer.GetWidgetFromXmlNode(_queryCountWidgetNode);
    Assert.NotNull(result);
    var typedResult = Assert.IsType<QueryCountWidget>(result); // 直接获取派生类实例
    var expected = new QueryCountWidget
    {
        // ... 预期对象初始化代码不变
    };
    Assert.Equivalent(expected, typedResult, true); // 比较派生类类型对象
}

方案2:自定义等价比较器

实现IEquivalenceComparer,在比较时过滤重复的Data属性,同时支持所有Widget类型:

public class WidgetEquivalenceComparer : IEquivalenceComparer
{
    public bool Equals(object? x, object? y, IEquivalenceComparerContext context)
    {
        if (x is null || y is null)
            return x == y;

        var type = x.GetType();
        if (type != y.GetType())
            return false;

        // 获取当前类型的所有公共实例属性,排除基类Widget中定义的Data属性
        var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Where(p => !(p.DeclaringType == typeof(Widget) && p.Name == "Data"));

        foreach (var prop in properties)
        {
            var xValue = prop.GetValue(x);
            var yValue = prop.GetValue(y);
            if (!context.Equals(xValue, yValue))
                return false;
        }

        // 单独比较派生类的Data属性(已重写的版本)
        var dataProp = type.GetProperty(nameof(Widget.Data), BindingFlags.Public | BindingFlags.Instance);
        if (dataProp != null)
        {
            var xData = dataProp.GetValue(x);
            var yData = dataProp.GetValue(y);
            if (!context.Equals(xData, yData))
                return false;
        }

        return true;
    }

    public int GetHashCode(object? obj, IEquivalenceComparerContext context)
    {
        return context.DefaultGetHashCode(obj);
    }
}

在测试中使用该比较器:

Assert.Equivalent(expected, result, options => options.Using(new WidgetEquivalenceComparer()));

方案3:排除基类的Data属性

通过断言选项排除基类中定义的Data属性,仅比较派生类的重写属性:

Assert.Equivalent(expected, result, options => 
    options.Exclude(p => p.DeclaringType == typeof(Widget) && p.Name == "Data"));

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 21:07:32