views:

1418

answers:

6

The following class serve as generic tester for equals/hashCode contract. It is a part of a home grown testing framework.

  • What do you think about?
  • How can I (strong) test this class?
  • It is a good use of Junit theories?

The class:

@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));
    }
}

usage:

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 = "";
}
+4  A: 

Joshua Bloch lays out the contract for hash code and equals in chapter 3 of "Effective Java". Looks like you covered a great deal of it. Check the document to see if I missed anything.

duffymo
also the javadoc for Object is very detailed
dfa
+6  A: 

One thing to consider: testing an object's conformance to the equals contract should involve instances of other types. In particular, problems are likely to appear with instances of a subclass or superclass. Joshua Bloch gives an excellent explanation of the related pitfalls in Effective Java (I'm reusing duffymo's link, so he should get credit for it) -- see the section under Transitivity involving the Point and ColorPoint classes.

True, your implementation doesn't prevent someone from writing a test that involves instances of a subclass, but because ObjectTest is a generic class it gives the impression that all data points should come from a single class (the class being tested). It might be better to remove the type parameter altogether. Just food for thought.

Bobby Eickhoff
indeed! Thanks, I'm removing the type parameter T.
dfa
+1  A: 

Love it, did you make one for compareTo??? and whould love to have a preview at you homebrew testing framework.

Thx, for the share

Frank
just added a compareToTester to my wishlist, do you like contribute code?
dfa
yes, your code was very usefull to me, and gave me inspiration to build some more. If you have you project as an open source one, i will be happy to contribute too.
Frank
+1  A: 

Really great ! I love it too. Do you plan to do the same with clone() ?

Benjamin
A: 

Maybe I'm missing something, but the equalsIsSymmetric test is in fact only correctly tested if you have to DataPoints which have the same values (e.g. String a = "a"; String a2 = "a";) Otherwise this test is only done when the 2 parameters are one instance (i.e. equalsIsSymmetric(a, a);). In fact you test again if equals obey the 'reflective' requirement instead of the symmetric requirement.

Martin Sturm
for this reason the test has `assumeThat(y.equals(x), is(true))`
dfa
yes, but in the current setup it is not able to create an 'x' and a 'y' for which holds x != y and x.equals(y), because the notEqualsWorks test will fail in that case. So the equalsIsSymmetric test is only performed for x and y where x == y.
Martin Sturm
yeah. Assuming the above setup, JUnit will execute: equalsIsSymmetric(a, a) and equalsIsSymmetric(b, b). Right?
dfa
A: 

The notEqualsWorks(Object x, Object y) theory is false: two distinct instances may still be logically equal according to their equals method; you're assuming instances are logically different if they're different references.

Using your own example above, the two distinct datapoints below (a != a2) are nevertheless equal but fail the notEqualsWorks test:

@DataPoint
public static String a = "a";
@DataPoint
public static String a2 = new String("a");
jorgetown
true but you should note that the theory has the following requirement: "User must provide datapoints that are not equal".
dfa