Search code examples
c#unit-testingfluent-assertions

Using FluentAssertions to compare two collections of dictionaries that are containing different types


From our database, we querying sets of records that are put in a collection of dynamic objects of type ExpandoObject that implements IDictionary<string, object> on the fields. These are the actual values.

From our SpecFlow BDD tests we get a collection of TableRows that implements IDictionary<string, string>. These are our expected values.

With FluentAssertions we'd like to test on equivalency on the whole collection with actual.Should().BeEquivalentTo(expected). Unfortunately, this won't work, because of the mismatch in types when the actual values are not of type string.

We could use actual.Should().BeEquivalentTo(expected, options => options.WithAutoConversion()), but this will make the whole actual set a collection of IDictionary<string, string> which is not useful for comparing dates.

I assembled a testcase that will show the same issue:

var expected = new List<Dictionary<string, string>>();
expected.Add(new Dictionary<string, string>
{
    {"Name", "Moon Inc."},
    {"Number", "42"},
    {"Date", "2018-12-31"}
});

var actual = new List<ExpandoObject>();
dynamic eo = new ExpandoObject();
eo.Name = "Moon Inc.";
eo.Number = 42;
eo.Date = new DateTime(2018, 12, 31);

actual.Add(eo);

actual.Should().BeEquivalentTo(expected, options => options);
/* 
    This throws:
    NUnit.Framework.AssertionException: 
    Expected item[0][Number] to be System.String, but found System.Int32.
    Expected item[0][Date] to be System.String, but found System.DateTime.
*/

actual.Should().BeEquivalentTo(expected, options => options.WithAutoConversion());
/*
    This throws:
    NUnit.Framework.AssertionException: 
    Expected item[0][Date] to be "2018-12-31" with a length of 10, 
    but "31-12-2018 0:00:00" has a length of 18.
/*

I tried to make the receiving type dynamic in a using method like:

actual.Should().BeEquivalentTo(expected, options => options
    .Using<dynamic>(ctx => ctx.Subject.Should().Be(ctx.Expectation)).WhenTypeIs<DateTime>()
    .Using<dynamic>(ctx => ctx.Subject.Should().Be(ctx.Expectation)).WhenTypeIs<int>());

/* 
    NUnit.Framework.AssertionException: 
    Expected item[0][Number] to be System.String, but found System.Int32.
    Expected item[0][Date] to be System.String, but found System.DateTime.
*/

Parsing both sides to a DateTime and using autoconversion does also not work because the actual type is not seen as a DateTime but as string:

actual.Should().BeEquivalentTo(expected, options => options
    .Using<dynamic>(
        ctx =>
            DateTime.ParseExact(ctx.Subject, "yyy-MM-dd", CultureInfo.InvariantCulture)
            .Should().Be(DateTime.ParseExact(ctx.Expectation, "yyy-MM-dd", CultureInfo.InvariantCulture)))
    .WhenTypeIs<DateTime>()
    .WithAutoConversion());

/*
    NUnit.Framework.AssertionException: 
    Expected item[0][Date] to be "2018-12-31" with a length of 10, 
    but "31-12-2018 0:00:00" has a length of 18.
*/

Is there any way with FluentAssertions to achieve this?


Solution

  • If nothing else helps, you can implement custom IEquivalencyStep like this:

    class WeakDateEquivalencyStep : IEquivalencyStep {
        public bool CanHandle(IEquivalencyValidationContext context, IEquivalencyAssertionOptions config) {
            if (context.IsRoot)
                return false;
            // handles situations when subject is date
            // but expectation is string
            return context.Subject is DateTime && context.Expectation is string;
        }
    
        public bool Handle(IEquivalencyValidationContext context, IEquivalencyValidator parent, IEquivalencyAssertionOptions config) {
            DateTime exp;
            // we know that expection is string here
            if (!DateTime.TryParse((string) context.Expectation, CultureInfo.InvariantCulture, DateTimeStyles.None, out exp)) {
                // do something, your spec is invalid
                throw new Exception($"Value {context.Expectation} does not represent valid date time");
            }
    
            context.Subject.Should().Be(exp, context.Because, context.BecauseArgs);
            return true;
        }
    }
    

    And then

    actual.Should().BeEquivalentTo(expected, options => 
       options.Using(new WeakDateEquivalencyStep()).WithAutoConversion());