JUnit理论:hashCode/equals契约

22
下面的类作为等同性/哈希码协定的通用测试器。它是一个自制测试框架的一部分。
  • 你怎么看待这个类?
  • 我该如何对这个类进行强大的测试?
  • 这是Junit理论的一个很好的使用案例吗?

这个类:

@Ignore
@RunWith(Theories.class)
public abstract class ObjectTest {

    // For any non-null reference value x, x.equals(x) should return true
    @Theory
    public void equalsIsReflexive(Object x) {
        assumeThat(x, is(not(equalTo(null))));
        assertThat(x.equals(x), is(true));
    }

    // For any non-null reference values x and y, x.equals(y) 
    // should return true if and only if y.equals(x) returns true.
    @Theory
    public void equalsIsSymmetric(Object x, Object y) {
        assumeThat(x, is(not(equalTo(null))));
        assumeThat(y, is(not(equalTo(null))));
        assumeThat(y.equals(x), is(true));
        assertThat(x.equals(y), is(true));
    }

    // For any non-null reference values x, y, and z, if x.equals(y)
    // returns true and y.equals(z) returns true, then x.equals(z) 
    // should return true.
    @Theory
    public void equalsIsTransitive(Object x, Object y, Object z) {
        assumeThat(x, is(not(equalTo(null))));
        assumeThat(y, is(not(equalTo(null))));
        assumeThat(z, is(not(equalTo(null))));
        assumeThat(x.equals(y) && y.equals(z), is(true));
        assertThat(z.equals(x), is(true));
    }

    // For any non-null reference values x and y, multiple invocations
    // of x.equals(y) consistently return true  or consistently return
    // false, provided no information used in equals comparisons on
    // the objects is modified.
    @Theory
    public void equalsIsConsistent(Object x, Object y) {
        assumeThat(x, is(not(equalTo(null))));
        boolean alwaysTheSame = x.equals(y);

        for (int i = 0; i < 30; i++) {
            assertThat(x.equals(y), is(alwaysTheSame));
        }
    }

    // For any non-null reference value x, x.equals(null) should
    // return false.
    @Theory
    public void equalsReturnFalseOnNull(Object x) {
        assumeThat(x, is(not(equalTo(null))));
        assertThat(x.equals(null), is(false));
    }

    // Whenever it is invoked on the same object more than once 
    // the hashCode() method must consistently return the same 
    // integer.
    @Theory
    public void hashCodeIsSelfConsistent(Object x) {
        assumeThat(x, is(not(equalTo(null))));
        int alwaysTheSame = x.hashCode();

        for (int i = 0; i < 30; i++) {
            assertThat(x.hashCode(), is(alwaysTheSame));
        }
    }

    // If two objects are equal according to the equals(Object) method,
    // then calling the hashCode method on each of the two objects
    // must produce the same integer result.
    @Theory
    public void hashCodeIsConsistentWithEquals(Object x, Object y) {
        assumeThat(x, is(not(equalTo(null))));
        assumeThat(x.equals(y), is(true));
        assertThat(x.hashCode(), is(equalTo(y.hashCode())));
    }

    // Test that x.equals(y) where x and y are the same datapoint 
    // instance works. User must provide datapoints that are not equal.
    @Theory
    public void equalsWorks(Object x, Object y) {
        assumeThat(x, is(not(equalTo(null))));
        assumeThat(x == y, is(true));
        assertThat(x.equals(y), is(true));
    }

    // Test that x.equals(y) where x and y are the same datapoint instance
    // works. User must provide datapoints that are not equal.
    @Theory
    public void notEqualsWorks(Object x, Object y) {
        assumeThat(x, is(not(equalTo(null))));
        assumeThat(x != y, is(true));
        assertThat(x.equals(y), is(false));
    }
}

用法:

import org.junit.experimental.theories.DataPoint;

public class ObjectTestTest extends ObjectTest {

    @DataPoint
    public static String a = "a";
    @DataPoint
    public static String b = "b";
    @DataPoint
    public static String nullString = null;
    @DataPoint
    public static String emptyString = "";
}

如果我理解正确的话,你的equalsIsSymmetric方法里最后一个语句应该是assertThat而不是assumeThat,对吧? - Bobby Eickhoff
那么,你想要一个自己开发的解决方案,但是你知道一些开源库可以做这些常见的测试吗?(我还建议使用可比较和可序列化的库。)我很有兴趣使用这样的框架。 - ivo
我目前没有看到这样的框架。我可以将这段代码贡献给一个开源项目(请参见Frank的回答)。 - dfa
1
@ivo:我已经在dollar中集成了这个类:http://bitbucket.org/dfa/dollar/src/tip/src/test/java/com/humaorie/dollar/integration/ObjectTest.java - dfa
5个回答

9
需要翻译的内容如下:

需要考虑一件事:测试对象符合等式协定应涉及其他类型的实例。特别是,子类或超类的实例可能会出现问题。Joshua Bloch在Effective Java中对相关陷阱进行了深入解释(我正在重用duffymo的链接,所以他应该得到相应的功劳)--请参见涉及Point和ColorPoint类的可传递性下的部分。

确实,您的实现无法防止某人编写涉及子类实例的测试,但由于ObjectTest是一个通用类,因此给人的印象是所有数据点都应来自单个类(被测试的类)。最好完全删除类型参数。这只是思考的食粮。


确实!谢谢,我正在移除类型参数 T。 - dfa

5

Joshua Bloch在《Effective Java》第3章中阐述了哈希码和相等性的契约。看起来你已经涵盖了很多内容。请查看文档,看看我是否遗漏了什么。


1
Object的Javadoc非常详细。 - dfa

0

notEqualsWorks(Object x, Object y)理论是错误的:根据它们的equals方法,两个不同的实例仍然可能在逻辑上相等;如果它们是不同的引用,您假设实例在逻辑上是不同的。

使用您自己上面的示例,下面的两个不同数据点(a!= a2)仍然相等,但未通过notEqualsWorks测试:

@DataPoint
public static String a = "a";
@DataPoint
public static String a2 = new String("a");

返回翻译后的文本:true,但您应该注意理论有以下要求:“用户必须提供不相等的数据点”。 - dfa

0

equalsWorks(Object x, Object y)方法执行的测试与equalsIsReflexive(Object x)相同。应该将其删除。

我认为应该删除notEqualsWorks(Object x, Object y),因为它会阻止使用相等的数据点进行其他理论测试,尽管整个测试都是关于拥有这样的对象。

没有这样的数据点,只有反身性被测试。


0
也许我漏掉了什么,但 equalsIsSymmetric 测试只有在两个具有相同值的数据点时才能正确测试(例如,String a =“a”; String a2 =“a”;)。否则,当这 2 个参数是一个实例时(即 equalsIsSymmetric(a,a)),才会执行此测试。事实上,您再次测试 equals 是否遵守“反射”要求,而不是对称要求。

由于这个原因,测试采用了 assumeThat(y.equals(x), is(true)) - dfa
是的,但在当前设置中,它无法创建一个'x'和'y',其中保持x!= y和x.equals(y),因为在这种情况下notEqualsWorks测试将失败。因此,equalsIsSymmetric测试仅针对x和y执行,其中x == y。 - Martin Sturm
假设以上设置,JUnit 将执行:equalsIsSymmetric(a, a) 和 equalsIsSymmetric(b, b)。对吗? - dfa

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