Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show the values of properties that are collections when comparing equivalence of collections of anonymous objects #2140

Closed
benagain opened this issue Mar 8, 2023 · 3 comments
Assignees

Comments

@benagain
Copy link
Contributor

benagain commented Mar 8, 2023

Description

When asserting equivalency of a collection of anonymous objects (using ContainsEquivalentOf or BeEquivalentTo) that contain properties which are themselves collections, the failure message does not report the contents of those properties.

The first test failure message (of the reproducible example below) shows the values of the collection in the property of both the expected and actual objects.

The only difference in the second test is that the objects are anonymous. Here the failure message does not show the values of the collections, instead it shows the collections' types.

I would be great if FluentAssertions could show the collections' values for anonymous objects the same way it does for named typed objects.

Reproduction Steps

using FluentAssertions;

public class X
{
    public List<string> Collection { get; set; }
}

public class UnitTest1
{
    [Fact]
    public void ContainsEquivalentWhenTyped()
    {
        var actual = new[]
        {
            new X
            {
                Collection = new List<string> { { "alhpa" } }
            }
        };

        var expected = new X
        {
            Collection = new List<string> { { "beta" } }
        };

        actual.Should().ContainEquivalentOf(expected);
        //  Expected actual {
        //      X
        //      {
        //          Collection = {"alhpa"}
        //      }
        //  }
        //   to contain equivalent of X
        //  {
        //      Collection = {"beta"}
        //  }.

    }

    [Fact]
    public void ContainsEquivalentWhenAnonymous()
    {
        var actual = new[]
        {
            new
            {
                Collection = new List<string> { { "alhpa" } }
            }
        };

        var expected = new
        {
            Collection = new List<string> { { "beta" } }
        };

        actual.Should().ContainEquivalentOf(expected);
        //   Expected actual {{ Collection = System.Collections.Generic.List`1[System.String] }}
        //    to contain equivalent of { Collection = System.Collections.Generic.List`1[System.String] }.
    }
}

Expected behavior

ContainEquivalentOf should produce a message containing the values of the collection

Expected actual {{ Collection = {"alpha"} }} to contain equivalent of { Collection = {"beta"} }.

Actual behavior

Expected actual {{ DictionaryProperty = System.Collections.Generic.List`1[System.String] }} to contain equivalent of { DictionaryProperty = System.Collections.Generic.List`1[System.String] }.

With configuration:
- Use declared types and members
- Compare enums by value
- Compare tuples by their properties
- Compare anonymous types by their properties
- Compare records by their members
- Include non-browsable members
- Match member by name (or throw)
- Be strict about the order of items in byte arrays
- Without automatic conversion.

Regression?

No response

Known Workarounds

No response

Configuration

net6.0
FluentAssertions v6.10.0

Other information

No response

@dennisdoomen
Copy link
Member

This is a limitation of the formatting code, not the specific implementation of BeEquivalentTo and ContainEquivalentOf.

@benagain
Copy link
Contributor Author

benagain commented Mar 9, 2023

Thanks for the pointer @dennisdoomen. I (think I) have tracked it down to the HasDefaultToStringImplementation check in the DefaultValueFormatter. The formatter outputs the types and member values unless there is a custom ToString, in which case that is used.

The problem comes down to the fact that anonymous types do have a custom ToString generated by the compiler, however that simply calls ToString on the anonymous type's properties, which doesn't recursively format the type.

It looks like one way to use FluentAssertion's own ability to write the member values would be to detect if the value being formatted is an anonymous object, (in addition to the existing check for a custom ToString). That does require a little bit of heuristic, which you may or may not object to?

public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild)
{
    ...

    if (HasDefaultToStringImplementation(value) || IsAnonymousType(value))
    {
        WriteTypeAndMemberValues(value, formattedGraph, formatChild);
    }
    else
    ...
}

private static bool IsAnonymousType(Type type)
{
    return type.Namespace is null
        && Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), inherit: false)
        && type.Name.Contains("AnonymousType", StringComparison.Ordinal)
        && (type.Name.StartsWith("<>", StringComparison.Ordinal)
            || type.Name.StartsWith("VB$", StringComparison.Ordinal));
}

This is the first time I've opened the source of FluentAssertions (which is very easy to navigate and a joy to work with - I wish other codebases I work on were so great!), you may know of a better way to implement this.

@dennisdoomen
Copy link
Member

dennisdoomen commented Mar 10, 2023

It looks like one way to use FluentAssertion's own ability to write the member values would be to detect if the value being formatted is an anonymous object, (in addition to the existing check for a custom ToString). That does require a little bit of heuristic, which you may or may not object to?

Yes, that looks like the right direction to me as well. I guess the same applies to records, since by default, we treat them the same as struct.

This is the first time I've opened the source of FluentAssertions (which is very easy to navigate and a joy to work with - I wish other codebases I work on were so great!), you may know of a better way to implement this.

Thank you for those kind words. Our goal is to make the code look like we want our real projects to look like too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants