我是否应该使用IEquatable来简化工厂测试?

时间:2015-11-29 21:41:40

标签: c# unit-testing factory-pattern iequatable

我经常使用代表工厂生产的实体的类。 为了便于轻松测试我的工厂,我通常会实施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();
    }
}

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

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

    Product actual = ProductFactory.BuildJames();

    Assert.AreEqual(expected, actual);
}

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

  1. GetHashCode并未实际使用,但我已花时间实施它。
  2. 我很少想在我的实际应用程序中使用Equals而不是测试。
  3. 我花了更多时间编写更多测试以确保Equals实际上正常工作。
  4. 我现在有另外三种维护方法,例如在类中添加一个属性,更新方法。
  5. 但是,这确实给了我一个非常整洁的TestMethod

    这是IEquatable的恰当用途,还是我应采取其他方法?

2 个答案:

答案 0 :(得分:4)

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

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

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

在C#中,通常使用struct作为值类型,使用class作为参考类型,但是您不必使用class。关键在于:

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

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

如果你的工厂正在创建一个值类型,那么请确保继续并使其成为IEquatable并使用这个巧妙的技巧。但在大多数情况下,我们不使用工厂作为值类型,这往往相当简单,我们使用工厂作为参考类型,这往往相当复杂,所以如果你的工厂正在创建一个引用类型,那么真的,这些各种对象只能通过引用进行比较,因此添加Equals()GetHashCode()方法可以从误导到错误。

从哈希映射的内容中得到一些提示:类型中Equals()GetHashCode()的存在通常意味着您可以将此类型的实例用作哈希映射中的键;但是如果对象不是一个不可变的值类型,那么它的状态可能会在放入地图后发生变化,在这种情况下GetHashCode()方法将开始评估其他东西,但哈希映射永远不会打扰-invoking 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" ) );

答案 1 :(得分:1)

对于测试代码,您可以使用基于反射的相等性,例如:Comparing object properties in c#

许多测试库提供了这样的实用程序,这样您就不必更改生产代码的设计以适应测试。