为什么Assert.AreEqual(x, y)失败,但Assert.AreEqual(y, x)不会?

4

考虑以下结构体:

public struct MyNumber
{
    private readonly int _value;

    public MyNumber(int myNumber)
    {
        _value = myNumber;
    }

    public int Value
    {
        get { return _value; }
    }

    public override bool Equals(object obj)
    {
        if (obj is MyNumber)
            return this == (MyNumber) obj;

        if (obj is int)
            return Value == (int)obj;

        return false;
    }

    public override string ToString()
    {
        return Value.ToString();
    }

    public static implicit operator int(MyNumber myNumber)
    {
        return myNumber.Value;
    }

    public static implicit operator MyNumber(int myNumber)
    {
        return new MyNumber(myNumber);
    }
}

在进行单元测试时,当我执行以下操作:
Assert.AreEqual(new MyNumber(123), 123);

这是绿色的。

但是这个测试失败了:

Assert.AreEqual(123, new MyNumber(123));

为什么会这样呢?我猜是因为int类决定了相等性,而在第一种情况中,我的类决定了相等性。但是我的类可以隐式转换为int,这难道不应该有所帮助吗?
如何使Assert.AreEqual在两种情况下都起作用?顺便说一下,我正在使用MSTest。
更新
实现IEquatable或IComparable也没有用。

想知道是否值得实现 IEquatable 接口? - Charleh
@Charleh:如果你要将值存储在集合中,实现IEquatable<T>是值得的。它可以加速像ContainsIndexOf等操作。 - Martin Liversage
不是 IEquatable @Charleh,而是 IComparable<int>。这将确保该类也能与 GreaterLess 断言一起使用。 - Rob Epstein
你正在使用哪个单元测试框架?你是使用 Microsoft.VisualStudio.TestTools.UnitTesting.Assert 进行断言,还是使用其他单元测试框架? - Martin Liversage
好问题,我确实在使用MSTest。 - Peter
3个回答

6
第一个断言将调用MyNumber.Equals,并且您已经以一种方式实现它,如果要比较的参数是Int32,并且它等于MyNumber的值,则可以成功。
第二个断言将调用Int32.Equals,它将失败,因为要比较的参数是一个MyNumber,而Int32不知道或不理解它。
您不能使第二个单元测试通过,因为您断言一个Int32应该等于您的值。但是它不同。决定第二个值是否相等的是Int32.Equals中的代码。您已经实现了一些转换运算符,可以对其进行单元测试,因此这些断言应该有效:
Assert.AreEqual(123, (int) new MyNumber(123));
Assert.AreEqual((MyNumber) 123, new MyNumber(123));

尽管已经采用了“隐式”的方式来实现转换,但是由于此方法要求的是两个类型为Object的参数,所以Assert.AreEquals不会自动调用它们,您需要像我上面那样显式地调用它们。
由于您在MyNumber.Equals中进行了特殊处理,所以现在您拥有的类型在equals方面不具有交换律,例如MyNumber(123) equals Int32(123)是true,但Int32(123) equals MyNumber(123)是false。您应该避免这种情况,建议您删除MyNumber.Equals中对int的特殊处理,并依靠通常情况下适用的“隐式”转换。当它们无法解决问题时,您将需要进行显式转换。

但是如果MyNumber是一个实现了IEquatable接口的类,我认为两个断言都将是正确的。不是吗? - Davecz
@Davecz:我认为Assert.AreEqual基本上映射到静态的Object.Equals,在进行一些简单的检查后,将调用objA.Equals(objB)。我不认为在这个过程中使用通用接口IEquatable<T> - Martin Liversage
按照我下面提到的实现IComparable将解决OP的问题。 - Rob Epstein
这个回答并不涵盖OP的问题:“如何使Assert.AreEqual在两种情况下都起作用?” - Rob Epstein
2
@RobEpstein: IComparable 是关于比较大小,例如用于排序。当比较相等性时,此接口不会使用,而这个问题似乎是关于相等性的比较。 - Martin Liversage
显示剩余3条评论

2
引用自Object.Equals
下列语句必须对所有Equals(Object)方法的实现结果均为真。在此列表中,x,y和z表示不为null的对象引用。
...
x.Equals(y)返回的值与y.Equals(x)返回的值相同。
您的Equals实现打破了此硬性要求。如果x不为null,则((object)x).Equals(123)必须返回与((object)123).Equals(x)相同的值,无论它的类型是什么。
有很多代码(正确地)假设询问哪个对象执行比较并不重要。设计您的代码,使得这种假设不被否定。
有效地,这意味着您必须设计您的类,以便它与任何整数类型都不相等,无论您喜欢与否。

0

将以下代码实现为您的MyNumber结构中的IComparable<int>

    public int CompareTo(int other)
    {
        return other.CompareTo(Value);
    }

这将确保所有的断言(Asserts)都按预期工作,例如:
    Assert.AreEqual(new MyNumber(123), 123);
    Assert.AreEqual(123, new MyNumber(123));
    Assert.Greater(124, new MyNumber(123));
    Assert.Less(124, new MyNumber(125));

结构体在比较两个MyNumber时的断言中按设计工作。 - Rob Epstein
这个不起作用。正如马丁在他的答案评论中提到的那样,IComporable用于排序和排序。它不会与Assert.AreEqual一起调用。 - Peter
这可能适用于您的单元测试框架,显然通过“Assert.Greater”和“Assert.Less”的出现与问题中使用的MSTest单元测试框架不同,但如果您使用MSTest,则会失败。 - Martin Liversage
我的回答时,原始问题并没有提到使用的测试框架,所以我提供了一个在 NUnit 中完美运行的答案。我会尝试找到一个在 MSTest 中也能工作的解决方案。 - Rob Epstein

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接