Microsoft.VisualStudio.TestTools.UnitTesting.Assert泛型方法重载行为

时间:2014-09-03 15:26:40

标签: c#

Microsoft.VisualStudio.TestTools.UnitTesting命名空间中,有一个方便的静态类Assert来处理测试中发生的断言。

让我烦恼的是,大多数方法都非常重载,而且最重要的是,它们具有通用版本。一个具体的例子是Assert.AreEqual,它有18个重载,其中包括:

Assert.AreEqual<T>(T t1, T t2)

这种通用方法有什么用?最初,我认为这是一种直接调用IEquatable<T> Equals(T t)方法的方法,但事实并非如此;它将始终调用非泛型版本object.Equals(object other)。在编写了相当多的单元测试之后我发现了很难的方法,期望这种行为(而不是像我应该的那样预先检查Assert类定义。)

为了调用Equals的通用版本,通用方法必须定义为:

Assert.AreEqual<T>(T t1, T t2) where T: IEquatable<T>

有没有充分的理由说明为什么不这样做?

是的,你失去了所有那些没有实现IEquatable<T>的类型的泛型方法,但是不管怎样,因为通过object.Equals(object other)来检查相等性,所以它不是一个很大的损失,所以Assert.AreEqual(object o1, object o2)已经足够好了。

当前的通用方法是否提供了我没有考虑的优势,或者只是没有人停下来思考它,因为它不是那么多的交易?我看到的唯一优势是参数类型安全,但这似乎很差。

修改:修正了一个错误,当我指的是IComparable时我一直引用IEquatable

3 个答案:

答案 0 :(得分:4)

由于constraints not being part of the signature的常见问题,具有该约束的方法将是不理想的。

问题在于,对于任何未被其自身特定重载覆盖的T,编译器会选择通用AreEqual<T>方法作为重载决策期间的最佳拟合,因为它确实是完全匹配的。在该过程的不同步骤中,编译器将评估T通过约束。对于任何未实现IEquatable<T>的T,此检查将失败,代码将无法编译。

考虑单元测试库代码的简化示例以及库中可能存在的类:

public static class Assert
{
    public static void AreEqual(object expected, object actual) { }
    public static void AreEqual<T>(T expected, T actual) where T : IEquatable<T> { }
}

class Bar { } 

Class Bar不在约束中实现接口。如果我们将以下代码添加到单元测试

Assert.AreEqual(new Bar(), new Bar());

由于对最佳候选方法的约束不满意,代码将无法编译。 (Bar替代T,这使其成为比object更好的候选者。)

  
    

类型&#39; Bar&#39;不能用作类型参数&#39; T&#39;在通用类型或方法中,Assert.AreEqual&lt; T&gt;(T,T)&#39;来自&#39; Bar&#39;没有隐式参考转换。到&#39; System.IEquatable&lt; Bar&gt;&#39;。

  

为了满足编译器并允许我们的单元测试代码进行编译和运行,我们必须将至少一个输入方法转换为object,以便可以选择非泛型重载,并且对于任何给定的T,这可能适用于您自己的代码或您希望在未实现该接口的测试用例中使用的代码。

Assert.AreEqual((object)new Bar(), new Bar());

所以必须提出这个问题 - 这是理想的吗?如果您正在编写单元测试库,您是否会创建一个具有这种不友好限制的方法?我怀疑你不会,并且Microsoft单元测试库的实现者(无论是否出于这个原因)也没有。

答案 1 :(得分:1)

基本上,泛型过载会强制您比较两个相同类型的对象。如果您更改了期望值或实际值,则会出现编译错误。这里MSDN blog很好地描述了它。

答案 2 :(得分:1)

你可以反编译该方法并看到它真正做的就是添加一个类型检查(通过ILSpy),在我看来甚至没有正确完成(它在相等之后检查类型):

public static void AreEqual<T>(T expected, T actual, string message, params object[] parameters)
{
    message = Assert.CreateCompleteMessage(message, parameters);
    if (!object.Equals(expected, actual))
    {
        string message2;
        if (actual != null && expected != null && !actual.GetType().Equals(expected.GetType()))
        {
            message2 = FrameworkMessages.AreEqualDifferentTypesFailMsg((message == null) ? string.Empty : Assert.ReplaceNulls(message), Assert.ReplaceNulls(expected), expected.GetType().FullName, Assert.ReplaceNulls(actual), actual.GetType().FullName);
        }
        else
        {
            message2 = FrameworkMessages.AreEqualFailMsg((message == null) ? string.Empty : Assert.ReplaceNulls(message), Assert.ReplaceNulls(expected), Assert.ReplaceNulls(actual));
        }
        Assert.HandleFailure("Assert.AreEqual", message2);
    }
}

理论上,它可以使用EqualityComparer<T>.Default来使用通用Equals(如果可用),但否则会回退到非泛型Equals。这样就不需要约束IEquatable<T>。对于通用和非通用Equals方法具有不同的行为是代码气味,IMO。

老实说,除非输入泛型参数,否则泛型重载并不是非常有用。我无法计算我错误输入属性的次数,或者比较了两种不同类型的属性并选择了AreEqual(object,object)重载。因此,在运行时间而不是编译时间给我一个失败。