EqualityTestCase

From EggeWiki
Revision as of 08:32, 23 March 2012 by Brianegge (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Overriding equals and hashCode in Java is generally useful for data type classes. Unfortunately, it's also easy to get wrong. Two common solutions are to use an IDE builder or call out to the Apache builder classes.

Regardless of the method, I use the following class to verify that I've met the equals and hashCode contracts. See also this page on stackoverflow.


JUnit 4

<geshi lang="java5"> import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue;

import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set;

import org.junit.Test;

/**

* This base class is for testing that an object properly overrides {@link Object#equals} and {@link Object#hashCode}.
* 

*

* Here are the important contract requirements for the two methods, as documented in the javadoc documentation for * java.lang.Object: *

*

    * *
  1. The hashCode method must return the same integer value every time it is invoked on the same object * during the entire execution of a Java application or applet. It need not return the same value for different runs of * an application or applet. The Java 2 platform (Java 2) documentation further allows the hashCode value * to change if the information used in the equals method changes.
    *
    *
  2. *
  3. If two objects are equal according to the equals method, they must return the same value from * hashCode.
    *
    *
  4. * *
  5. The equals method is reflexive, which means that an object is equal to itself: * x.equals(x) should return true.
    *
    *
  6. *
  7. The equals method is symmetric: If x.equals(y) returns true, then * y.equals(x) should return true also.
    *
    *
  8. * *
  9. The equals method is transitive: If x.equals(y) returns true and * y.equals(z) returns true, then x.equals(z) should return true.
    *
    *
  10. *
  11. The equals method is consistent. x.equals(y) should consistently return either true or * false. The Java 2 javadoc clarifies that the result of x.equals(y) can change if the information used in * the equals comparisons change.
    *
    *
  12. * *
  13. Finally, x.equals(null) should return false.
  14. *
* 
* Additionally, if this class implements {@link Comparable}, {@link Serializable}, or {@link Cloneable}, then the
* object will be checked to see if these methods provide reasonable behavior.
* 
* You must implement two factory methods: getA, getB.
* 
* If the object under test implements Comparable then a < b (< c)
* 
* @author Brian Egge
*/

public abstract class EqualityTestCase<T> {

   /**
    * 
    * @return a new instance of an Object which should equate to other objects created by getA, but be not equals to
    *         objects created by getB
    */
   protected abstract T getA() throws Exception;
   /**
    * 
    * @return a new instance of an Object which should equate to other objects created by getB, but be not equals to
    *         objects created by getA
    */
   protected abstract T getB() throws Exception;
   /**
    * @return an object which is not equal to either A or B. This method can optionally be implemented.
    */
   protected T getC() throws Exception {
       return null;
   }
   @Test
   public void testEquals() throws Exception {
       assertFalse(getA().equals(null));
       assertFalse(getB().equals(null));
       assertFalse(getA().equals(new Object()));
       assertFalse(getB().equals(new Object()));
       // The equals method is symmetric: If x.equals(y) returns true, then y.equals(x) should return true also.
       assertTrue(getA().equals(getA()));
       assertTrue(getB().equals(getB()));
       assertFalse(getA().equals(getB()));
       assertFalse(getB().equals(getA()));
       if (getC() != null) {
           assertFalse(getC().equals(null));
           assertFalse(getC().equals(new Object()));
           assertTrue(getC().equals(getC()));
           assertFalse(getA().equals(getC()));
           assertFalse(getC().equals(getA()));
           assertFalse(getB().equals(getC()));
           assertFalse(getC().equals(getB()));
       }
       for (Iterator<T> iterator = getObjects().iterator(); iterator.hasNext();) {
           Object o = iterator.next();
           // The equals method is reflexive, which means that an object is equal to itself: x.equals(x) should return
           // true.
           assertTrue(o.equals(o));
           // x.equals(null) should return false.
           assertFalse(o.equals(null));
       }
   }
   @Test
   public void testHashCode() throws Exception {
       assertEquals("hashCodes differ for two A objects", getA().hashCode(), getA().hashCode());
       assertEquals(getB().hashCode(), getB().hashCode());
       assertFalse("hashCode is either not implmented correctly or is very bad\n"
               + "Both A and B returned a hashCode of " + getA().hashCode(), getA().hashCode() == getB().hashCode());
       if (getC() != null) {
           assertEquals(getC().hashCode(), getC().hashCode());
           assertFalse("hashCode is either not implmented correctly or is very bad\n"
                   + "Both A and C returned a hashCode of " + getA().hashCode(), getA().hashCode() == getC()
                   .hashCode());
           assertFalse("hashCode is either not implmented correctly or is very bad\n"
                   + "Both B and C returned a hashCode of " + getB().hashCode(), getB().hashCode() == getC()
                   .hashCode());
       }
       Set<T> set = new HashSet<T>();
       set.add(getA());
       assertTrue(set.contains(getA()));
       assertFalse(set.contains(getB()));
       assertFalse(set.contains(getC()));
   }
   @Test
   @SuppressWarnings("unchecked")
   // Comparable<T> is checked only at runtime
   public void testComparable() throws Exception {
       if (getA() instanceof Comparable) {
           assertEquals(0, ((Comparable<T>) getA()).compareTo(getA()));
           assertEquals(0, ((Comparable<T>) getB()).compareTo(getB()));
           assertEquals("Item B should compare equally with another item B", 0,
                   ((Comparable<T>) getB()).compareTo(getB()));
           assertTrue("Item B should be less than Item A", ((Comparable<T>) getA()).compareTo(getB()) < 0);
           assertTrue(((Comparable<T>) getB()).compareTo(getA()) > 0);
           if (getC() != null) {
               assertEquals(0, ((Comparable<T>) getC()).compareTo(getC()));
               assertTrue("Item C should be less than Item A", ((Comparable<T>) getA()).compareTo(getC()) < 0);
               assertTrue("Item C should be less than Item B", ((Comparable<T>) getB()).compareTo(getC()) < 0);
               assertTrue(((Comparable<T>) getC()).compareTo(getB()) > 0);
           }
       }
   }
   @Test
   public void testSerializable() throws Exception {
       if (getA() instanceof Serializable) {
           for (T o : getObjects()) {
               assertTrue(o.equals(serialize(o)));
           }
       }
   }
   private Object serialize(Object value) throws IOException, ClassNotFoundException {
       ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream();
       ObjectOutputStream outputStream = new ObjectOutputStream(outputBuffer);
       outputStream.writeObject(value);
       outputStream.flush();
       byte[] bytes = outputBuffer.toByteArray();
       ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
       ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
       return objectInputStream.readObject();
   }
   /**
    * If you must implement {@link java.lang.Cloneable}, please do it correctly! The clone interface doesn't require
    * you to make 'clone' a public method, but this what one generally expects for an object which implements
    * {@link java.lang.Cloneable}
    * 
    * @throws NoSuchMethodException
    * @throws SecurityException
    * @throws InvocationTargetException
    * @throws IllegalAccessException
    * @throws IllegalArgumentException
    */
   @Test
   public void testClone() throws Exception, IllegalAccessException, InvocationTargetException {
       if (getA() instanceof Cloneable) {
           for (T o : getObjects()) {
               Method clone = o.getClass().getMethod("clone", new Class[] {});
               Object cloned = clone.invoke(o, new Object[] {});
               assertNotSame(o, cloned);
               assertEquals(o, cloned);
               assertEquals(o.getClass(), cloned.getClass());
           }
       }
   }
   /**
    * toString doesn't have to be implemented. There's not a lot of checks we can do to make sure this is a 'good'
    * toString, other than to make sure the code works and doesn't return null.
    */
   @Test
   public void testToString() throws Exception {
       for (Iterator<T> iterator = getObjects().iterator(); iterator.hasNext();) {
           Object o = iterator.next();
           assertNotNull(o.toString());
       }
   }
   /**
    * This will either return a list of one or two objects depending on if getC() is implemented.
    * 
    * @return a list of test objects.
    */
   private List<T> getObjects() throws Exception {
       List<T> list = new ArrayList<T>(3);
       list.add(getA());
       list.add(getB());
       if (getC() != null) {
           list.add(getC());
       }
       return list;
   }

} </geshi>

JUnit 3

<geshi lang="java5">

import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set;

import junit.framework.TestCase;

/**

* This base class is for testing that an object properly overrides
* {@link Object#equals} and {@link Object#hashCode}.
* 

*

* Here are the important contract requirements for the two methods, as * documented in the javadoc documentation for java.lang.Object: *

*

    * *
  1. The hashCode method must return the same integer value * every time it is invoked on the same object during the entire execution of a * Java application or applet. It need not return the same value for different * runs of an application or applet. The Java 2 platform (Java 2) documentation * further allows the hashCode value to change if the information * used in the equals method changes.
    *
    *
  2. *
  3. If two objects are equal according to the equals method, * they must return the same value from hashCode.
    *
    *
  4. * *
  5. The equals method is reflexive, which means that an * object is equal to itself: x.equals(x) should return true.
    *
    *
  6. *
  7. The equals method is symmetric: If * x.equals(y) returns true, then y.equals(x) * should return true also.
    *
    *
  8. * *
  9. The equals method is transitive: If * x.equals(y) returns true and y.equals(z) * returns true, then x.equals(z) should return true.
    *
    *
  10. *
  11. The equals method is consistent. x.equals(y) * should consistently return either true or false. The Java 2 javadoc clarifies * that the result of x.equals(y) can change if the information * used in the equals comparisons change.
    *
    *
  12. * *
  13. Finally, x.equals(null) should return false.
  14. *
* 
* Additionally, if this class implements {@link Comparable},
* {@link Serializable}, or {@link Cloneable}, then the object will be checked
* to see if these methods provide reasonable behavior.
* 
* You must implement two factory methods: getA, getB.
* 
* If the object under test implements Comparable then a < b (< c)
*/

public abstract class EqualityTestCase<T> extends TestCase {

/** * * @return a new instance of an Object which should equate to other objects * created by getA, but be not equals to objects created by getB */ protected abstract T getA() throws Exception;

/** * * @return a new instance of an Object which should equate to other objects * created by getA, but be not equals to objects created by getB */ protected abstract T getB() throws Exception;

/** * @return an object which is not equal to either A or B. This method can * optionally be implemented. */ protected T getC() throws Exception { return null; }

public void testEquals() throws Exception { assertFalse(getA().equals(null)); assertFalse(getB().equals(null)); assertFalse(getA().equals(new Object())); assertFalse(getB().equals(new Object())); //The equals method is symmetric: If x.equals(y) returns true, then y.equals(x) should return true also. assertTrue(getA().equals(getA())); assertTrue(getB().equals(getB())); assertFalse(getA().equals(getB())); assertFalse(getB().equals(getA())); if (getC() != null) { assertFalse(getC().equals(null)); assertFalse(getC().equals(new Object())); assertTrue(getC().equals(getC())); assertFalse(getA().equals(getC())); assertFalse(getC().equals(getA())); assertFalse(getB().equals(getC())); assertFalse(getC().equals(getB())); }

for (Iterator<T> iterator = getObjects().iterator(); iterator.hasNext();) { Object o = iterator.next(); // The equals method is reflexive, which means that an object is equal to itself: x.equals(x) should return true. assertTrue(o.equals(o)); // x.equals(null) should return false. assertFalse(o.equals(null)); } }

public void testHashCode() throws Exception { assertEquals("hashCodes differ for two A objects", getA().hashCode(), getA().hashCode()); assertEquals(getB().hashCode(), getB().hashCode()); assertFalse("hashCode is either not implmented correctly or is very bad\n" + "Both A and B returned a hashCode of " + getA().hashCode(), getA().hashCode() == getB().hashCode()); if (getC() != null) { assertEquals(getC().hashCode(), getC().hashCode()); assertFalse("hashCode is either not implmented correctly or is very bad\n" + "Both A and C returned a hashCode of " + getA().hashCode(), getA().hashCode() == getC() .hashCode()); assertFalse("hashCode is either not implmented correctly or is very bad\n" + "Both B and C returned a hashCode of " + getB().hashCode(), getB().hashCode() == getC() .hashCode()); }

Set<T> set = new HashSet<T>(); set.add(getA());

assertTrue(set.contains(getA())); assertFalse(set.contains(getB())); assertFalse(set.contains(getC())); }

@SuppressWarnings("unchecked") // Comparable<T> is checked only at runtime public void testComparable() throws Exception { if (getA() instanceof Comparable) { assertEquals(0, ((Comparable<T>) getA()).compareTo(getA())); assertEquals(0, ((Comparable<T>) getB()).compareTo(getB())); assertEquals("Item B should compare equally with another item B", 0, ((Comparable<T>) getB()).compareTo(getB()));

assertTrue("Item B should be less than Item A", ((Comparable<T>) getA()).compareTo(getB()) < 0); assertTrue(((Comparable<T>) getB()).compareTo(getA()) > 0); if (getC() != null) { assertEquals(0, ((Comparable<T>) getC()).compareTo(getC())); assertTrue("Item C should be less than Item A", ((Comparable<T>) getA()).compareTo(getC()) < 0); assertTrue("Item C should be less than Item B", ((Comparable<T>) getB()).compareTo(getC()) < 0); assertTrue(((Comparable<T>) getC()).compareTo(getB()) > 0); } } }

public void testSerializable() throws Exception { if (getA() instanceof Serializable) { for (T o : getObjects()) { assertTrue(o.equals(serialize(o))); } } }

private Object serialize(Object value) throws IOException, ClassNotFoundException { ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(); ObjectOutputStream outputStream = new ObjectOutputStream(outputBuffer); outputStream.writeObject(value); outputStream.flush(); byte[] bytes = outputBuffer.toByteArray(); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); return objectInputStream.readObject(); }

/** * If you must implement {@link java.lang.Cloneable}, please do it correctly! The clone interface doesn't require * you to make 'clone' a public method, but this what one generally expects for an object which * implements {@link java.lang.Cloneable} * * @throws NoSuchMethodException * @throws SecurityException * @throws InvocationTargetException * @throws IllegalAccessException * @throws IllegalArgumentException */ public void testClone() throws Exception, IllegalAccessException, InvocationTargetException { if (getA() instanceof Cloneable) { for (T o : getObjects()) { Method clone = o.getClass().getMethod("clone", new Class[] {}); Object cloned = clone.invoke(o, new Object[] {}); assertNotSame(o, cloned); assertEquals(o, cloned); assertEquals(o.getClass(), cloned.getClass()); } } }

/** * toString doesn't have to be implemented. There's not a lot of checks we can do to make sure * this is a 'good' toString, other than to make sure the code works and doesn't return null. */ public void testToString() throws Exception { for (Iterator<T> iterator = getObjects().iterator(); iterator.hasNext();) { Object o = iterator.next(); assertNotNull(o.toString()); } }


/** * This will either return a list of one or two objects depending on if getC() is implemented. * @return a list of test objects. */ private List<T> getObjects() throws Exception { List<T> list = new ArrayList<T>(3); list.add(getA()); list.add(getB()); if (getC() != null) { list.add(getC()); } return list; } } </geshi>