Search code examples
c#.netxunitcovariance

xUnit Assert.Equivalent throws an System.ArgumentException when trying to test an abstract property with covariant return type


I am using xUnit 2.6.3 (latest stable) to test a .Net 8 project. I have an object that inherits from an abstract class, and the method I'm testing has a return type of the base class. The base class has an abstract property called Data of type WidgetData that only has a getter, and the derived class overrides that property by returning an object of type QueryCountWidgetData, which inherits from WidgetData. There is no setter.

I can step through debugging and verify that the returned object matches my expected test object, but when I call Assert.Equivalent(expected, result, true), xUnit throws a System.ArgumentException with the message: An item with the same key has already been added. Key: Data. It's being thrown from AssertHelper.GetGettersForType(Type type) method. My object definitions and test code are below. Am I doing something wrong, does xUnit not accommodate covariant return types yet, or is this something entirely different?

Widget Class

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 Class

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

QueryCountWidget Class

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 and QueryCountWidgetData classes

public abstract class WidgetData
{ }

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

Test Method (_queryCountWidgetNode is populated in a test fixture; the exception fails at the Assert.Equivalent call at the end)

[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);
}

I started off using an older version of xUnit but I've updated xunit to 2.6.3 and xunit.runner.visualstudio to 2.5.5; both are the latest stable versions. I've verified that I can call Assert.Equivalent on each property individually without issue. I've also changed the Widget class to test:

  1. Changed Widget.Data to virtual instead of abstract with QueryCountWidget.Data still overriding: same exception
  2. Changed Widget.Data to virtual instead of abstract with QueryCountWidget.Data as new: same exception
  3. Removed Widget.Data property and let QueryCountWidget.Data just return as a get only property: test completes successfully
  4. Changed Widget.Data to be of type QueryCountWidgetData with QueryCountWidget.Data still overriding: test completes successfully

Unfortunately, the only two changes that seem to work preclude me from using these classes the way I want to. I have a dozen other types of widget, each with their own associated WidgetData derived type. I really don't want to have to test each property individually because that doesn't account for future development if new properties get added to the Widget classes. Is there some way to get this to work with xUnit without mangling my class structures?


Solution

  • Your issue with Assert.Equivalent in xUnit seems to be stemming from a conflict in how xUnit's assertion mechanism handles properties in inherited classes, especially when dealing with overridden properties.

    In xUnit, Assert.Equivalent is designed to compare two objects for structural equality, meaning it checks if they have the same data, not necessarily being the same instance. However, when it encounters overridden properties, especially in a scenario with covariant return types like yours, it may not handle the comparison correctly, leading to exceptions like the one you encountered.

    I would suggest you use the popular NuGet package FluentAssertions and replace this line of code:

    Assert.Equivalent(expected, result, true);
    

    with:

    result.Should().BeEquivalentTo(expected);
    

    This works as expected, without throwing any errors and is more readable in my opinion.