我应该使用IEquatable来简化工厂的测试吗?

Jam*_*ood 5 c# unit-testing iequatable factory-pattern

我经常使用代表工厂生产的实体的类.为了使我的工厂简单的测试很容易,我通常执行IEquatable<T>,同时也覆盖GetHashCodeEquals(由所建议的MSDN).

例如; 采用以下实体类,这是为了示例目的而简化的.通常我的类有更多属性.偶尔会有一个集合,在Equals我检查的方法中使用SequenceEqual.

public class Product : IEquatable<Product>
{
    public string Name
    {
        get;
        private set;
    }

    public Product(string name)
    {
        Name = name;
    }

    public override bool Equals(object obj)
    {
        if (obj == null)
        {
            return false;
        }

        Product product = obj as Product;

        if (product == null)
        {
            return false;
        }
        else
        {
            return Equals(product);
        }
    }

    public bool Equals(Product other)
    {
        return Name == other.Name;
    }

    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
}
Run Code Online (Sandbox Code Playgroud)

这意味着我可以像这样进行简单的单元测试(假设构造函数在其他地方进行了测试).

[TestMethod]
public void TestFactory()
{
    Product expected = new Product("James");

    Product actual = ProductFactory.BuildJames();

    Assert.AreEqual(expected, actual);
}
Run Code Online (Sandbox Code Playgroud)

然而,这提出了一些问题.

  1. GetHashCode 实际上没有使用,但我花时间实现它.
  2. 我实际上很少想Equals在我的实际应用中使用而不是测试.
  3. 我花了更多时间编写更多测试以确保Equals实际工作正常.
  4. 我现在有另外三种方法来维护,例如向类添加属性,更新方法.

但是,这确实让我非常整洁TestMethod.

这是适当的使用IEquatable,还是应该采取另一种方法?

Mik*_*kis 4

这是否是一个好主意实际上取决于您的工厂创建什么样的类型。有两种类型:

  • 具有值语义的类型(简称值类型)和

  • 具有引用语义的类型(简称引用类型)。

在 C# 中,通常用于struct值类型和class引用类型,但您不必这样做,您可以class同时使用两者。重点是:

  • 值类型是小型的、通常不可变的、独立的对象,其主要目的是包含某个值,而

  • 引用类型是具有复杂可变状态的对象,可能引用其他对象,以及重要的功能,即算法、业务逻辑等。

如果你的工厂正在创建一个值类型,那么当然,继续制作它IEquatable并使用这个巧妙的技巧。但在大多数情况下,我们不会将工厂用于值类型,这往往相当简单,我们将工厂用于引用类型,这往往相当复杂,因此,如果您的工厂正在创建引用类型,那么实际上,这些类型的对象只能通过引用进行比较,因此添加Equals()andGetHashCode()方法绝对不会产生误导甚至错误。

从哈希映射发生的情况中得到一点提示:类型中Equals()和的存在GetHashCode()通常意味着您可以使用该类型的实例作为哈希映射中的键;但是如果对象不是不可变值类型,那么它的状态在放入映射后可能会发生变化,在这种情况下,该GetHashCode()方法将开始评估其他内容,但哈希映射永远不会GetHashCode()为了重新调用而烦恼。重新定位地图中的对象。这种情况的结果往往是混乱。

因此,最重要的是,如果您的工厂创建复杂的对象,那么您也许应该采取不同的方法。显而易见的解决方案是调用工厂,然后检查返回对象的每个属性以确保它们都符合预期。

我也许可以对此提出改进,但请注意,我只是想到了它,我从未尝试过,所以在实践中它可能是也可能不是一个好主意。这里是:

您的工厂可能会创建实现特定接口的对象。(否则,拥有工厂有什么意义,对吧?)因此,理论上您可以规定新创建的实现此接口的对象实例应该具有初始化为特定值集的某些属性。这将是接口强加的规则,因此您可以将一些函数绑定到接口来检查这是否为真,并且该函数甚至可以通过一些提示进行参数化,以便在不同情况下期望不同的初始值。

(最后我检查过,在 C# 中,与接口绑定的方法通常是扩展方法;我完全不记得 C# 是否允许静态方法成为接口的一部分,或者 C# 的设计者是否还添加了语言的简洁和优雅就像 Java 的默认接口方法一样。)

因此,使用扩展方法,它可能看起来像这样:

public boolean IsProperlyInitializedInstance( this IProduct self, String hint )
{
    if( self.Name != hint )
        return false;
    //more checks here
    return true;
}

IProduct product = productFactory.BuildJames();
Assert.IsTrue( product.IsProperlyInitializedInstance( hint:"James" ) );
Run Code Online (Sandbox Code Playgroud)