Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Assert.AreEqual on custom struct with implicit conversion operator fail?

I've created a custom struct to represent an amount. It is basically a wrapper around decimal. It has an implicit conversion operator to cast it back to decimal.

In my unit test, I assert that the Amount equals the original decimal value, but the test fails.

[TestMethod]
public void AmountAndDecimal_AreEqual()
{
    Amount amount = 1.5M;

    Assert.AreEqual(1.5M, amount);
}

When I use an int though (for which I did not create a conversion operator), the test does succeed.

[TestMethod]
public void AmountAndInt_AreEqual()
{
    Amount amount = 1;

    Assert.AreEqual(1, amount);
}

When I hover the AreEqual, it shows that the first one resolves to

public static void AreEqual(object expected, object actual);

and the second one leads to

public static void AreEqual<T>(T expected, T actual);

It looks like the int value 1 is implicitly cast to an Amount, while the decimal value 1.5M is not.

I don't understand why this happens. I would've expected just the opposite. The first unit test should be able to cast the decimal to an Amount.

When I add an implicit cast to int (which wouldn't make sense), the second unit test also fails. So adding an implicit cast operator breaks the unit test.

I have two questions:

  1. What is the explanation for this behavior?
  2. How can I fix the Amount struct so both tests will succeed?

(I know I could change the test to do an explicit conversion, but if I don't absolutely have to, I won't)

My Amount struct (just a minimal implementation to show the problem)

public struct Amount
{
    private readonly decimal _value;

    private Amount(decimal value)
    {
        _value = value;
    }

    public static implicit operator Amount(decimal value)
    {
        return new Amount(value);
    }

    public static implicit operator decimal(Amount amount)
    {
        return amount._value;
    }
}
like image 810
comecme Avatar asked Nov 24 '16 19:11

comecme


1 Answers

Bad things happen when you can convert implicitly in both directions, and this is one example.

Because of the implicit conversions the compiler is able to pick Assert.AreEqual<decimal>(1.5M, amount); and Assert.AreEqual<Amount>(1.5M, amount); with equal value.*

Since they're equal, neither overload will be picked by inference.

Since there's no overload to pick by inference, neither make it into the list for picking the best-match and only the (object, object) form is available. So it's the one picked.

With Assert.AreEqual(1, amount) then since there is an implicit conversion from int to Amount (via implicit int->decimal) but no implicit conversion from Amount to int the compiler thinks "obviously they mean the Assert.AreEqual<Amount>() here"†, and so it is picked.

You can explicitly pick an overload with Assert.AreEqual<Amount>() or Assert.AreEqual<decimal>() but you're probably better off making one of your conversions the "narrowing" from that has to be explicit if at all possible because this feature of your struct will hurt you again. (Hurrah for unit tests finding flaws).


*Another valid overload choice is to pick Assert.AreEqual<object>, but it's never picked by inference because:

  1. Both the rejected overloads were considered even better.
  2. It is always beaten by the non-generic form that takes object anyway.

As such it can only ever be called by including the <object> in the code.

†The compiler treats everything said to it as either obvious in meaning or completely incomprehensible. There are people like that too.

like image 100
Jon Hanna Avatar answered Sep 30 '22 13:09

Jon Hanna