fluentassertions: BeEquivalentTo on non-generic dictionary gives confusing error

Description

upgrading from v5 to version 6.12.0 I no longer am able to assert exception data

Reproduction Steps

 [Test]
public void exception()
{
    Func<Task> act = async () =>
    {
        var ex = new Exception("msg");
        ex.Data.Add("id", 22);
        ex.Data.Add("CustomerId", 33);
        throw ex;
    };
    
    act.Should()
        .ThrowAsync<Exception>()
        .WithMessage("msg")
        .Result.Which.Should().BeEquivalentTo(new Dictionary<object,object>()
        {
            {"id",22},
            {"CustomerId",33}
        });
}

Expected behavior

assert

Actual behavior

Expected act to be a dictionary or collection of key-value pairs that is keyed to type System.Object. It implements .

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.

   at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
   at FluentAssertions.Execution.TestFrameworkProvider.Throw(String message)
   at FluentAssertions.Execution.CollectingAssertionStrategy.ThrowIfAny(IDictionary`2 context)
   at FluentAssertions.Equivalency.EquivalencyValidator.AssertEquality(Comparands comparands, EquivalencyValidationContext context)
   at FluentAssertions.Primitives.ObjectAssertions`2.BeEquivalentTo[TExpectation](TExpectation expectation, Func`2 config, String because, Object[] becauseArgs)
   at FluentAssertions.Primitives.ObjectAssertions`2.BeEquivalentTo[TExpectation](TExpectation expectation, String because, Object[] becauseArgs)
   at Alka.PermissionsService.Tests.AgreementRegistrationTests.exception() in C:\source\xxx.Tests\Tests.cs:line 

Regression?

yes

Known Workarounds

you can build this but its ugly and the assert requires to call this

public static Dictionary<string, object> Extract(this Exception e)
{
    var result = new Dictionary<string, object>();
    var ee = e.Data.GetEnumerator();
    while (ee.MoveNext())
    {
        result.Add(ee.Key.ToString(), ee.Value);
    }
    return result;
}

Configuration

No response

Other information

No response

Are you willing to help with a pull-request?

No

About this issue

  • Original URL
  • State: closed
  • Created 10 months ago
  • Comments: 17 (10 by maintainers)

Most upvoted comments

I mean I just spent time upgrading Frome v5 to v6 so if possible I’d like not to spend additional time going fromnv6 to v7.

We spent 17 months writing v7 🙃

We try to follow semantic versioning such that all minor and patch updates should just be bumping the nuget version. So upgrading a major version will often include some work on the consumer side and even here we carefully consider if a change is justified to minimize the hassle of upgrading.

We both have a changelog for v6 (as well as every other release) and made a migration guide to v6. While the migration guide doesn’t specifically mentions this case (as we do consider it a regression), the release notes mentions both dropping some support for non-generic collections and reworking equivalency for collection types.

We designed this library for extensibility, so until we release v7 with a proper fix for this bug, you can use the extension code below to use utilize method overload resolution to match Dictionary<,> to IDictionary.

var subject = new ListDictionary
{
	{ "id", 22 },
	{ "CustomerId", 33 }
};

var expected = new Dictionary<object, object>
{
	{ "id", 22 },
	{ "CustomerId", 33 }
};

subject.Should().BeEquivalentTo(expected);
using FluentAssertions;
using FluentAssertions.Primitives;
using System.Collections.Generic;
using System.Collections;
using FluentAssertions.Equivalency;

static class IDictionaryExtensions
{
	public static IDictionaryAssertions Should(this IDictionary obj) => new IDictionaryAssertions(obj);
}

class IDictionaryAssertions : ObjectAssertions<IDictionary, IDictionaryAssertions>
{
    public IDictionaryAssertions(IDictionary obj) : base(obj)
    {}
	
    /// <summary>
    /// Asserts that an object is equivalent to another object.
    /// </summary>
    /// <remarks>
    /// Objects are equivalent when both object graphs have equally named properties with the same value,
    /// irrespective of the type of those objects. Two properties are also equal if one type can be converted to another and the result is equal.
    /// The type of a collection property is ignored as long as the collection implements <see cref="IEnumerable{T}"/> and all
    /// items in the collection are structurally equal.
    /// Notice that actual behavior is determined by the global defaults managed by <see cref="AssertionOptions"/>.
    /// </remarks>
    /// <param name="expectation">The expected element.</param>
    /// <param name="because">
    /// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
    /// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
    /// </param>
    /// <param name="becauseArgs">
    /// Zero or more objects to format using the placeholders in <paramref name="because"/>.
    /// </param>
    public AndConstraint<IDictionaryAssertions> BeEquivalentTo(IDictionary expectation, string because = "",
        params object[] becauseArgs)
    {
        return BeEquivalentTo(expectation, config => config, because, becauseArgs);
    }
	
	/// <summary>
    /// Asserts that an object is equivalent to another object.
    /// </summary>
    /// <remarks>
    /// Objects are equivalent when both object graphs have equally named properties with the same value,
    /// irrespective of the type of those objects. Two properties are also equal if one type can be converted to another and the result is equal.
    /// The type of a collection property is ignored as long as the collection implements <see cref="IEnumerable{T}"/> and all
    /// items in the collection are structurally equal.
    /// </remarks>
    /// <param name="expectation">The expected element.</param>
    /// <param name="config">
    /// A reference to the <see cref="EquivalencyAssertionOptions{TSubject}"/> configuration object that can be used
    /// to influence the way the object graphs are compared. You can also provide an alternative instance of the
    /// <see cref="EquivalencyAssertionOptions{TSubject}"/> class. The global defaults are determined by the
    /// <see cref="AssertionOptions"/> class.
    /// </param>
    /// <param name="because">
    /// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
    /// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
    /// </param>
    /// <param name="becauseArgs">
    /// Zero or more objects to format using the placeholders in <paramref name="because"/>.
    /// </param>
    /// <exception cref="ArgumentNullException"><paramref name="config"/> is <see langword="null"/>.</exception>
    public AndConstraint<IDictionaryAssertions> BeEquivalentTo(IDictionary expectation,
        Func<EquivalencyAssertionOptions<IDictionary>, EquivalencyAssertionOptions<IDictionary>> config, string because = "",
        params object[] becauseArgs)
    {
        return base.BeEquivalentTo(expectation, config => config, because, becauseArgs);
    }
}