0

I want to be able to compare any two things that are IConvertible to DateTime using NUnit's Assert.That, preferably without creating a custom constraint. I have a simple IComparer that does the trick nicely. It works with EqualTo().Using(), so long as both the actual and expected are of the same type. It looks like the EqualsConstraint's AdjustArgumentIfNeeded method is failing the assertion before my IComparer gets to do its job.

What can I do to allow any test of the form Assert.That(actual, Is.EqualTo(expected).Using(DateTimeComparer.Instance)); to pass if actual and expected can be converted to the same date time, and fail otherwise?

Here's an MCVE which shows two tests passing when I compare dates converted from different format strings, but failing when comparing a converted date with a real DateTime.

using NUnit.Framework;
namespace NunitTest
{
    public class DateTimeComparer : IComparer
    {
        public static readonly DateTimeComparer Instance = new DateTimeComparer();

        public int Compare(object x, object y)
        {
                var dateTime1 = Convert.ToDateTime(x);
                var dateTime2 = Convert.ToDateTime(y);
                return dateTime1.CompareTo(dateTime2);
        }
    }

    [TestFixture]
    public class DateTimeComparerTest
    {
        [Test]
        public void TestComparerUsingString()
        {
            // Passes
            Assert.That("2 August 2016",
                        Is.EqualTo("02/08/2016")
                          .Using(DateTimeComparer.Instance));
        }

        [Test]
        public void TestComparerUsingDateTime()
        {
            // Passes
            Assert.That(new DateTime(2016, 8, 2),
                        Is.EqualTo(new DateTime(2016, 8, 2))
                          .Using(DateTimeComparer.Instance));
        }

        [Test]
        public void TestComparerUsingExpectedDateTime()
        {
            // Fails
            Assert.That("2 August 2016",
                        Is.EqualTo(new DateTime(2016, 8, 2))
                          .Using(DateTimeComparer.Instance));
        }

        [Test]
        public void TestComparerUsingActualDateTime()
        {
            // Fails
            Assert.That(new DateTime(2016, 8, 2),
                        Is.EqualTo("2 August 2016")
                          .Using(DateTimeComparer.Instance));
        }
    }
}

Debugging shows that the Compare method is entered in the two passing cases and is not entered in the two failing cases.

Paul Hicks
  • 13,289
  • 5
  • 51
  • 78
  • My workaround for the moment is to call `ToString()` on all my actuals and expecteds. This is obviously hacky, and exposes me to possible culture issues (since there are still a few people who read "02/08/2016" as some time in February...), but it is working for now. – Paul Hicks Aug 02 '16 at 03:37

2 Answers2

1

This is an issue you'd have to file within NUnit to fully resolve. I believe the cause of the observed behavior is found in EqualityAdapter.cs in the CanCompare method:

public virtual bool CanCompare(object x, object y)
{
    if (x is string && y is string)
        return true;

    if (x is IEnumerable || y is IEnumerable)
        return false;

    return true;
}

This returns true if the arguments are both strings or neither is a string, but not if one is a string and the other isn't (string is an IEnumerable via IEnumerable<char>). If CanCompare returns false it won't even go into your custom comparer to see if the arguments are equal and the framework will run the default comparison logic. I don't believe CanCompare is overridden when used with any of the basic Using(IComparer) type constraints.

Mike Zboray
  • 39,828
  • 3
  • 90
  • 122
  • Yes and I'd bet that's not what Charlie had in mind with that piece of code. Perhaps it should be "if one of them is IEnumerable and the other isn't". Even then, `CanCompare` should be skipped if `Using` is provided. – Paul Hicks Aug 02 '16 at 04:04
  • Good catch! Sombody file it. :-) – Charlie Aug 02 '16 at 05:12
  • Filed at [GitHub](https://github.com/nunit/nunit/issues/1897). Sorry it took so long, I completely forgot about it! I'm underworked at the moment, so I'll try to build NUnit, write some tests etc. I won't claim the issue until I can build though. – Paul Hicks Nov 09 '16 at 22:03
  • Sorry for unaccepting. Your answer is still helpful. But the alternative implementation (using the generic adapter instead of the vanilla one) is more helpful to future answer-seekers. – Paul Hicks Nov 10 '16 at 00:44
1

After further investigation, I found that the correct behaviour was already implemented, just not in the default EqualityAdapter. Using any of the generic adapters works. That is, changing the comparer in the question to this makes the tests pass:

public class DateTimeComparer : IComparer<IConvertible>
{
    public static readonly DateTimeComparer Instance = new DateTimeComparer();

    public int Compare(IConvertible x, IConvertible y)
    {
        var dateTime1 = Convert.ToDateTime(x);
        var dateTime2 = Convert.ToDateTime(y);
        return dateTime1.CompareTo(dateTime2);
    }
}
Paul Hicks
  • 13,289
  • 5
  • 51
  • 78