1

Is there a way, with FluentAssertions, to control how the values of a dictionary are compared for equality ?

I have a class, one property of which is a dictionary (string/double). I would like to compare two instances of the class (expected and actual), and for the dictionary members, to specify how "equality" is determined.

Assuming I have a class as shown:

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestMethod1()
    {
        var t1 = new Thing();
        t1.Name = "Bob";
        t1.Values.Add("Hello", 100.111);
        t1.Values.Add("There", 100.112);
        t1.Values.Add("World", 100.113);

        var t2 = new Thing();
        t2.Name = "Bob";
        t2.Values.Add("Hello", 100.111);
        t2.Values.Add("There", 100.112);
        t2.Values.Add("World", 100.1133);

        t1.Should().BeEquivalentTo(t2);
    }
}

public class Thing
{
    public string Name { get; set; }

    public Dictionary<string, double> Values { get; set; } = new Dictionary<string, double>();
}

I would like to be able to specify how, for example, the "World" entry in the dictionary is compared. In reality, it may be that the values could be very large, or the same over 10 decimal places (but not thereafter) but I think I may need to say something like "the same if less than 1% difference).

I like the way FluentAssertions tells me the member and why they are not the same, and have tried a custom IAssertionRule (using Options lambda), but that only seemed to compare the class properties, not the members of the dictionary.

I don't own the classes being compared, so cannot override the "Equal" method, and I cannot find a way to specify a custom comparer (IEquatable) - but I suspect I would lose the fluent details of why they are not the same.

If it were possible, but that any method would also apply to the doubles that were properties of the Class (as opposed to values in the dictionary), that would be ok.

Thanks.

Black Light
  • 2,358
  • 5
  • 27
  • 49
  • Following Nkosi's post below, does anyone know how "BeApproximately" is implemented, and whether one could write a version which compares by percentage difference (with the useful failure reason) ? – Black Light Jun 25 '18 at 08:31
  • I posted an answer as it involved a fair chunk of code, but to be honest you should probably ask another question, and delete that answer! – Michal Ciechan Jun 25 '18 at 10:42

3 Answers3

4

BeApproximately can be used to compare the doubles within an acceptable range of precision. Using that together with configuring the precision for all doubles should satisfy the desired behavior.

t1.Should().BeEquivalentTo(t2, options => options
    .Using<double>(ctx => 
        ctx.Subject.Should().BeApproximately(ctx.Expectation, ctx.Expectation * 0.01D))
    .WhenTypeIs<double>()
);

Reference Object graph comparison: Equivalency Comparison Behavior

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Thanks Nkosi, that works well if the difference is one of precision, but not if the difference is a percentage (ie. > 1% difference between expected and actual). – Black Light Jun 25 '18 at 08:29
  • @BlackLight I see your point. In that case your could have simple set the precision to `ctx.Expectation * 0.01`. But the other answer given should work as well. Glad you found a solution. – Nkosi Jun 25 '18 at 11:49
1

Following on from Nkosi answer, this is an example of a BeApproximately that I am using (to allow using BeApproximately with a decimal?):

    [CustomAssertion]
    public static void BeApproximately(this NullableNumericAssertions<decimal> value, decimal? expected, decimal precision, string because = "",
        params object[] becauseArgs)
    {
        if (expected == null)
            value.BeNull(because);
        else
        {
            if (!Execute.Assertion.ForCondition(value.Subject != null).BecauseOf(because)
                .FailWith($"Expected {{context:subject}} to be '{expected}' {{reason}} but found null"))
                return;

            Decimal num = Math.Abs(expected.Value - (Decimal) value.Subject);

            Execute.Assertion.ForCondition(num <= precision).BecauseOf(because, becauseArgs).FailWith("Expected {context:value} to approximate {1} +/- {2}{reason}, but {0} differed by {3}.", (object) value.Subject, (object) expected.Value, (object) precision, (object) num);
        }
    }
Michal Ciechan
  • 13,492
  • 11
  • 76
  • 118
0

Following on from Michal's excellent post, I have started working with the following, which shows excellent promise:

    [CustomAssertion]
    public static void BeWithinPercentageOf(this NumericAssertions<double> value, double expected, double tolerance, string because = "", params object[] becauseArgs)
    {
        if (!Execute.Assertion.ForCondition(value.Subject != null)
            .BecauseOf(because)
            .FailWith($"Expected {{context:subject}} to be '{expected}' {{reason}} but found null"))
            return;

        var actual = (double)value.Subject;
        var diff = Math.Abs(expected - actual);

        if (diff > double.Epsilon)
        {
            var percent = Math.Round(100 / (expected / diff), 2);

            Execute.Assertion.ForCondition(percent <= tolerance)
                .BecauseOf(because, becauseArgs)
                .FailWith("Expected {context:value} to be {1} (±{2}%){reason}, but {0} differed by {3}%.", actual, expected, tolerance, percent);
        }
    }
Black Light
  • 2,358
  • 5
  • 27
  • 49