一个单元应该如何testinghashCode-equals契约?

简而言之,根据Java的object.hashCode(),hashCode合约:

  1. 散列码不应该改变,除非影响equals()的东西改变了
  2. equals()意味着散列码是==

让我们假设主要关注不可变的数据对象 – 它们的信息在构造之后永远不会改变,所以假定#1被保持。 这留下#2:问题只是确认等于暗示哈希码==。

显然,我们不能testing每一个可以想象的数据对象,除非这个数据对象是非常小的。 那么,编写一个可能赶上常见情况的unit testing的最好方法是什么呢?

由于这个类的实例是不可变的,所以构造这样一个对象的方法是有限的。 如果可能的话,这个unit testing应该覆盖所有这些。 closures我的头顶,入口点是构造函数,反序列化和子类的构造函数(应该简化为构造函数调用问题)。

[我要通过研究来回答我自己的问题。 来自其他StackOverflowers的input对此过程是一个受欢迎的安全机制。]

[这可能适用于其他OO语言,所以我添加了该标签。]

EqualsVerifier是一个相对较新的开源项目,它在testing平等合同方面做得非常好。 它没有GSBase的EqualsTester所具有的问题 。 我肯定会推荐它。

我的build议是考虑为什么/如何不可能实现,然后编写一些针对这些情况的unit testing。

例如,假设您有一个自定义的Set类。 如果两个集合包含相同的元素,则两个集合是相等的,但如果这两个元素以不同的顺序存储,则两个相等集合的基础数据结构可能会有所不同。 例如:

 MySet s1 = new MySet( new String[]{"Hello", "World"} ); MySet s2 = new MySet( new String[]{"World", "Hello"} ); assertEquals(s1, s2); assertTrue( s1.hashCode()==s2.hashCode() ); 

在这种情况下,集合中元素的顺序可能会影响它们的哈希,具体取决于您实现的哈希algorithm。 所以这是我写的testing,因为它testing的情况下,我知道一些哈希algorithm可能产生不同的结果,我定义为相等的两个对象。

你应该使用与你自己的自定义类相似的标准,不pipe是什么。

我会推荐GSBase的EqualsTester。 它基本上是你想要的。 我有两个(次要的)问题,

  • 构造函数完成所有的工作,我不认为这是好的做法。
  • 当A类的一个实例等于A类的一个子类的一个实例时,它就失败了。这不一定是违反了等价契约。

[在撰写本文时,另外三个答案已经发布。]

重申一遍,我的问题的目的是find标准的testing用例来确认hashCodeequals是否相互一致。 我对这个问题的解决方法是想象程序员在编写有问题的类时所采用的通用path,即不可变数据。 例如:

  1. 写入equals()而不写入hashCode() 这常常意味着平等被定义为意味着两个领域的平等。
  2. 在不写equals()情况下写hashCode() equals() 这可能意味着程序员正在寻求更高效的哈希algorithm。

在#2的情况下,这个问题似乎对我来说是不存在的。 没有额外的实例被创build为equals() ,所以不需要额外的实例拥有相同的哈希代码。 在最坏的情况下,散列algorithm可能会产生散列映射的性能较差,这是超出了这个问题的范围。

在#1的情况下,标准unit testing需要使用传递给构造函数的相同数据创build同一对象的两个实例,并validation相同的哈希代码。 假阳性呢? 可以select构造函数参数,这些参数只是在一个不合理的algorithm上产生相同的散列码。 倾向于避免这样的参数的unit testing将满足这个问题的精神。 这里的快捷方式是检查equals()的源代码,认真思考,然后编写一个基于此的testing,但是在某些情况下这可能是必要的,但也可能有一些常见的testing来解决常见的问题,履行这个问题的精神。

例如,如果要testing的类(称为Data)具有一个构造函数,该构造函数接受一个String,并且从equals() Strings构造的实例产生了equals()实例,那么一个好的testing可能会testing:

  • new Data("foo")
  • 另一个new Data("foo")

我们甚至可以检查new Data(new String("foo"))的哈希码,以强制string不被拦截,虽然这更有可能产生一个正确的哈希码比Data.equals()是产生一个正确的结果,在我看来。

Eli Courtwright的回答是基于等规范的知识努力打破散列algorithm的一个例子。 一个特殊的集合的例子是一个很好的例子,因为用户创build的Collection有时会出现,而且在散列algorithm中很容易混乱。

这是我在testing中有多个断言的唯一情况之一。 既然你需要testingequals方法,你还应该同时检查hashCode方法。 所以在你的每个equals方法testing用例上也要检查hashCode合约。

 A one = new A(...); A two = new A(...); assertEquals("These should be equal", one, two); int oneCode = one.hashCode(); assertEquals("HashCodes should be equal", oneCode, two.hashCode()); assertEquals("HashCode should not change", oneCode, one.hashCode()); 

当然,检查一个好的hashCode是另一个练习。 老实说,我不会麻烦做双重检查,以确保哈希码不会在同一运行中改变,这种问题更好地处理,抓住代码审查,并帮助开发人员理解为什么这不是一个好方法编写hashCode方法。

如果我有一个类的Thing ,像其他人一样,我写一个类ThingTest ,它持有该类的所有unit testing。 每个ThingTest都有一个方法

  public static void checkInvariants(final Thing thing) { ... } 

如果Thing类重写hashCode并且等于它有一个方法

  public static void checkInvariants(final Thing thing1, Thing thing2) { ObjectTest.checkInvariants(thing1, thing2); ... invariants that are specific to Thing } 

该方法负责检查所有被devise为容纳在任何一对Thing对象之间的不variables。 它委托给的ObjectTest方法负责检查任何一对对象之间必须保持的所有不variables。 由于equalshashCode是所有对象的方法,该方法检查hashCodeequals是否一致。

然后我有一些testing方法可以创buildThing对象对,并将它们传递给checkInvariants成对方法。 我使用等价划分来决定哪些对值得testing。 我通常创build每个对只有一个属性是不同的,再加上一个testing两个等价对象的testing。

我也有时会有一个3参数checkInvariants方法,虽然我发现findinf缺陷不太有用,所以我不经常这样做