11
Apr
10

Annotation based Equals and HashCode Test Case


This page will describe how you can annotate a Java bean so that JUnit verify proper equals() and hashCode() implementations.

Background

The fastest way to implement equals and hash code is to manually code it. The problem with doing it manually is that the code become out of sync.

Solution

Annotate the fields that should be present in the equals and hashCode() methods. JUnit checks these annotations and verifies that the equals() and hashCode() methods are implemented properly. You still need to manually update the equals and hashcode methods if something changes but now at least you have a test case that will fail if something goes wrong.

Requirements

  • Java 5 or better
  • Maven 2 – (Link to a Maven Tutorial available on right navigation)

Implementation

The following example will demonstrate the very basic usage scenario of annotation based unit testing of equals and hashCode methods.

First step is to create a project using maven archetype. Open up the command prompt and navigate to an empty directory.

mvn archetype:generate -DarchetypeArtifactId=maven-archetype-quickstart

groupId: com.test
artifactId: beanUnitTest

Answer the rest of the questions with defaults “Just hit the enter key”

If you are using eclipse then you may want to regenerate your project before importing it into eclipse.
do this by typing:

mvn eclipse:clean eclipse:eclipse

Next define the equals annotation. This annotation will be used to mark the fields that should participate in the equals and hashCode methods.

/src/test/java/com/test/Equals.java

package com.test;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Equals {
}

/src/main/java/com/test/TestBean.java

package com.test;

public class TestBean {
	@Equals
	private String name;
	@Equals
	private String address;
	@Equals
	private boolean drugTestPassed;
	@Equals
	private int age;
	@Equals
	private long birthday;
	@Equals
	private SubBean subBean = new SubBean();

// getters and setters go here...

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((address == null) ? 0 : address.hashCode());
		result = prime * result + age;
		result = prime * result + (int) (birthday ^ (birthday >>> 32));
		result = prime * result + (drugTestPassed ? 1231 : 1237);
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		result = prime * result + ((subBean == null) ? 0 : subBean.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		TestBean other = (TestBean) obj;
		if (address == null) {
			if (other.address != null)
				return false;
		} else if (!address.equals(other.address))
			return false;
		if (age != other.age)
			return false;
		if (birthday != other.birthday)
			return false;
		if (drugTestPassed != other.drugTestPassed)
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		if (subBean == null) {
			if (other.subBean != null)
				return false;
		} else if (!subBean.equals(other.subBean))
			return false;

		return true;
	}
}

src/main/java/com/test/SubBean.java

package com.test;

/**
 * This is a sub bean where only the ID field is used to decide
 * if the object is equal or not.
 *
 */
public class SubBean {

	@Equals
	private Integer id;

	private String name;
	private String age;

// getters and setters...

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		SubBean other = (SubBean) obj;
		if (id == null) {
			if (other.id != null)
				return false;
		} else if (!id.equals(other.id))
			return false;
		return true;
	}
}

The following is the heart of the system. It is just an abstract base class that could be extended. It was taken from http://blog.coryfoy.com/2008/05/unit-testing-equals-and-hashcode-of-java-beans/ and modified to utilize the annotation features of Java 5. Also some changes were made to not traverse object attributes (see appendix below for more details).

/src/test/java/com/test/BeanTestCase.java

package com.test;
import junit.framework.TestCase;

import java.lang.reflect.Field;

public abstract class BeanTestCase extends TestCase {

	private static final String TEST_STRING_VAL1 = "Some Value";
	private static final String TEST_STRING_VAL2 = "Some Other Value";

	/**
	 * TODO: currently there is no way to check if there are extra checks in the equal.
	 * TODO: what about testing equals in subclasses?
	 * 
	 * @param classUnderTest
	 */
	public static void _assertMeetsEqualsContract(Class classUnderTest) {
		Object o1;
		Object o2;
		try {
			// Get Instances
			o1 = classUnderTest.newInstance();
			o2 = classUnderTest.newInstance();
			assertTrue(
					"Instances with default constructor not equal (o1.equals(o2))",
					o1.equals(o2));
			assertTrue(
					"Instances with default constructor not equal (o2.equals(o1))",
					o2.equals(o1));

			for (Field field : classUnderTest.getDeclaredFields()) {
	        	if(field.isAnnotationPresent(Equals.class)) {
					// Reset the instances
					o1 = classUnderTest.newInstance();
					o2 = classUnderTest.newInstance();

					field.setAccessible(true);

					// set field it to ValueA

					if (field.getType() == String.class) {
						field.set(o1, TEST_STRING_VAL1);
					} else if (field.getType() == boolean.class) {
						field.setBoolean(o1, true);
					} else if (field.getType().isAssignableFrom(Boolean.class)) {
						field.set(o1, Boolean.TRUE);
					} else if (field.getType() == short.class) {
						field.setShort(o1, (short) 1);
					} else if (field.getType().isAssignableFrom(Short.class)) {
						field.set(o1, Short.valueOf((short)1));
					} else if (field.getType() == long.class) {
						field.setLong(o1, (long) 1);
					} else if (field.getType().isAssignableFrom(Long.class)) {
						field.set(o1, Long.valueOf(1));
					} else if (field.getType() == float.class) {
						field.setFloat(o1, (float) 1);
					} else if (field.getType().isAssignableFrom(Float.class)) {
						field.set(o1, Float.valueOf(1));
					} else if (field.getType() == int.class) {
						field.setInt(o1, 1);
					} else if (field.getType().isAssignableFrom(Integer.class)) {
						field.set(o1, Integer.valueOf(1));
					} else if (field.getType() == byte.class) {
						field.setByte(o1, (byte) 1);
					} else if (field.getType().isAssignableFrom(Byte.class)) {
						field.set(o1, Byte.valueOf((byte)1));
					} else if (field.getType() == char.class) {
						field.setChar(o1, (char) 1);
					} else if (field.getType().isAssignableFrom(Character.class)) {
						field.set(o1, Character.valueOf((char)1));
					} else if (field.getType() == double.class) {
						field.setDouble(o1, (double) 1);
					} else if (field.getType().isAssignableFrom(Double.class)) {
						field.set(o1, Double.valueOf(1));
					} else if (field.getType().isEnum()) {
						field.set(o1, field.getType().getEnumConstants()[0]);
					} else if (Object.class.isAssignableFrom(field.getType())) {
						 System.out.println("got here " + field.getType());
						 field.set(o1, field.getType().newInstance());
						 field.set(o2, null);
					} else {
						fail("Don't know how to set a " + field.getType().getName());
					}

					assertFalse("Instances with o1 having " + field.getName()
							+ " set and o2 having it not set are equal", o1
							.equals(o2));

					field.set(o2, field.get(o1));

					assertTrue(
							"After setting o2 with the value of the object in o1, the two objects in the field are not equal",
							field.get(o1).equals(field.get(o2)));

					assertTrue(
							"Instances with o1 having "
									+ field.getName()
									+ " set and o2 having it set to the same object of type "
									+ field.get(o2).getClass().getName()
									+ " are not equal", o1.equals(o2));

					// set field it to ValueB

					if (field.getType() == String.class) {
						field.set(o2, TEST_STRING_VAL2);
					} else if (field.getType() == boolean.class) {
						field.setBoolean(o2, false);
					} else if (field.getType().isAssignableFrom(Boolean.class)) {
						field.set(o2, Boolean.FALSE);
					} else if (field.getType() == short.class) {
						field.setShort(o2, (short) 0);
					} else if (field.getType().isAssignableFrom(Short.class)) {
						field.set(o2, Short.valueOf((short)0));
					} else if (field.getType() == long.class) {
						field.setLong(o2, (long) 0);
					} else if (field.getType().isAssignableFrom(Long.class)) {
						field.set(o2, Long.valueOf(0));
					} else if (field.getType() == float.class) {
						field.setFloat(o2, (float) 0);
					} else if (field.getType().isAssignableFrom(Float.class)) {
						field.set(o2, Float.valueOf(0));
					} else if (field.getType() == int.class) {
						field.setInt(o2, 0);
					} else if (field.getType().isAssignableFrom(Integer.class)) {
						field.set(o2, Integer.valueOf(0));
					} else if (field.getType() == byte.class) {
						field.setByte(o2, (byte) 0);
					} else if (field.getType().isAssignableFrom(Byte.class)) {
						field.set(o2, Byte.valueOf((byte)0));
					} else if (field.getType() == char.class) {
						field.setChar(o2, (char) 0);
					} else if (field.getType().isAssignableFrom(Character.class)) {
						field.set(o2, Character.valueOf((char)0));
					} else if (field.getType() == double.class) {
						field.setDouble(o2, (double) 0);
					} else if (field.getType().isAssignableFrom(Double.class)) {
						field.set(o2, Double.valueOf(0));
					} else if (field.getType().isEnum()) {
						field.set(o2, field.getType().getEnumConstants()[1]);
					} else if (Object.class.isAssignableFrom(field.getType())) { // if its an object.
						//field.set(o2, field.getType().newInstance());
						field.set(o2, null);
					} else {
						fail("Don't know how to set a " + field.getType().getName());
					}

					// make the final asserts after setting the field to be
					// different.

					assertFalse(
							"After setting o2 with a different object than what is in o1, the two objects in the field are equal. "
									+ "This is after an attempt to walk the fields to make them different",
							field.get(o1).equals(field.get(o2)));
					assertFalse(
							"Instances with o1 having "
									+ field.getName()
									+ " set and o2 having it set to a different object are equal",
							o1.equals(o2));
	        	}
			}

		} catch (InstantiationException e) {
			e.printStackTrace();
			throw new AssertionError(
					"Unable to construct an instance of the class under test");
		} catch (IllegalAccessException e) {
			e.printStackTrace();
			throw new AssertionError(
					"Unable to construct an instance of the class under test");
		}
	}

	
	public static void _assertMeetsHashCodeContract(Class classUnderTest) {
		try {
			for (Field field : classUnderTest.getDeclaredFields()) {
	        	if(field.isAnnotationPresent(Equals.class)) {
					Object o1 = classUnderTest.newInstance();
					int initialHashCode = o1.hashCode();

					field.setAccessible(true);
					if (field.getType() == String.class) {
						field.set(o1, TEST_STRING_VAL1);
					} else if (field.getType() == boolean.class) {
						field.setBoolean(o1, true);
					} else if (field.getType().isAssignableFrom(Boolean.class)) {
						field.set(o1, Boolean.TRUE);
					} else if (field.getType() == short.class) {
						field.setShort(o1, (short) 1);
					} else if (field.getType().isAssignableFrom(Short.class)) {
						field.set(o1, (short)1);
					} else if (field.getType() == long.class) {
						field.setLong(o1, (long) 1);
					} else if (field.getType().isAssignableFrom(Long.class)) {
						field.set(o1, Long.valueOf(1));
					} else if (field.getType() == float.class) {
						field.setFloat(o1, (float) 1);
					} else if (field.getType().isAssignableFrom(Float.class)) {
						field.set(o1, Float.valueOf(1));
					} else if (field.getType() == int.class) {
						field.setInt(o1, 1);
					} else if (field.getType().isAssignableFrom(Integer.class)) {
						field.set(o1, Integer.valueOf(1));
					} else if (field.getType() == byte.class) {
						field.setByte(o1, (byte) 1);
					} else if (field.getType().isAssignableFrom(Byte.class)) {
						field.set(o1, Byte.valueOf((byte)1));
					} else if (field.getType() == char.class) {
						field.setChar(o1, (char) 1);
					} else if (field.getType().isAssignableFrom(Character.class)) {
						field.set(o1, Character.valueOf((char)1));
					} else if (field.getType() == double.class) {
						field.setDouble(o1, (double) 1);
					} else if (field.getType().isAssignableFrom(Double.class)) {
						field.set(o1, Double.valueOf(1));
					} else if (field.getType().isEnum()) {
						field.set(o1, field.getType().getEnumConstants()[0]);
					} else if (Object.class.isAssignableFrom(field.getType())) {
						if(field.get(o1) == null) {
							field.set(o1, field.getType().newInstance());							
						} else {
							field.set(o1, null);
						}
					} else {
						fail("Don't know how to set a " + field.getType().getName());
					}
					int updatedHashCode = o1.hashCode();
					assertFalse(
							"The field "
									+ field.getName()
									+ " was not taken into account for the hashCode contract ",
							initialHashCode == updatedHashCode);
	        		
	        	}
			}
		} catch (InstantiationException e) {
			e.printStackTrace();
			throw new AssertionError(
					"Unable to construct an instance of the class under test");
		} catch (IllegalAccessException e) {
			e.printStackTrace();
			throw new AssertionError(
					"Unable to construct an instance of the class under test");
		}
	}
}

The following test case is designed to test the equals and hashcode of the TestBean class.

src/test/java/com/test/TestBeanTest.java

package com.test;

public class TestBeanTest extends BeanTestCase {

	public void testEqualsContractMet() {
		assertMeetsEqualsContract(TestBean.class);
	}
	public void testHashCodeContractMet() {
		assertMeetsHashCodeContract(TestBean.class);
	}
}

Run the above test case and it should pass.

mvn test

The next class we will extend the TestBean and introduce the open member variable in the sub class. We will write a test case that will check for the proper implementation of the equals and hashcode.

src/main/java/com/test/TestBeanSubClass.java

package com.test;

public class TestBeanSubClass extends TestBean {
	@Equals
	private boolean open;

// getters setters

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = super.hashCode();
		result = prime * result + (open ? 1231 : 1237);
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (!super.equals(obj))
			return false;
		if (getClass() != obj.getClass())
			return false;
		TestBeanSubClass other = (TestBeanSubClass) obj;
		if (open != other.open)
			return false;
		return true;
	}
}

The following is a test case that will check for the proper implementation of the equals and hashcode method.

src/test/java/com/test/TestBeanSubClassTest.java

package com.test;

public class TestBeanSubClassTest extends BeanTestCase {
	public void testEqualsContractMet2() {
		assertMeetsEqualsContract(TestBeanSubClass.class);
	}
	public void testHashCodeContractMet2() {
		assertMeetsHashCodeContract(TestBeanSubClass.class);
	}
}

References

The implementation here differs from the one described in the following link in the fact that reflection is NOT used during runtime.

http://www.platypusinnovation.com/view.html?id=201

Base implementation of the above code was taken from the following URL.

http://blog.coryfoy.com/2008/05/unit-testing-equals-and-hashcode-of-java-beans/

Library to generate various boilerplate code during runtime.

http://projectlombok.org/

Appendix

Dealing with Object Attributes

Object Attributes are expected to implement their own equals method. The test case that checks the parent’s equals method should not be responsible for checking the equals method of object attributes. Instead it will only check to see if the parent bean’s equals() method calls the sub bean’s equals as part of the comparison if the sub bean was marked with the @Equals annotation. The sub bean should be tested separately using its own test case.

We can check to see if equals method is called by exploiting the contract It has with the system.

  • Contract #1: The equals() method returns true when you pass in the same object reference as an argument. Example: this.equals(this) results in true.
  • Contract #2 The equals() method returns false when you pass in a null as the argument. Example: this.equals(null) results in false.

During our test case we will create 2 instances of TestBean class and set instance of SubBean in both instances to be the same reference. We will call the testBeanA.equals(testBeanB) method and it should return true. We will change one instance of TestBean to have a null reference for the subBean attribute and call the testBeanA.equals(testBeanB) method. It should return false since testBeanA.subBean has a reference and testBeanB.subBean is null.

Example: the following “testBeanA.getSubBean().equals(testBeanB.getSubBean())” will return false

Advertisements

3 Responses to “Annotation based Equals and HashCode Test Case”


  1. April 11, 2010 at 6:53 pm

    Howdy,

    Great idea! I’m glad you were able to build on the concept from my blog. Thanks for the link over to it as well.

    Cory

  2. April 21, 2010 at 10:01 pm

    Fixed the BeanTestCase to take into account Reference Types for primitives (Integer, Double, Long, etc…)

  3. 3 Kunal Naik
    December 15, 2011 at 2:07 pm

    This came in very handy for me, thanks for sharing!
    I had a use case where I wanted to run these tests on a subclass of an abstract base class so I added the following lines before the for-loop to get all the declared fields from the base class(es) as well:

    ArrayList allClassFields = new ArrayList();
    allClassFields.addAll(Arrays.asList(classUnderTest.getDeclaredFields()));
    Class superClass = classUnderTest.getSuperclass();
    while(superClass != Object.class) {
    allClassFields.addAll(Arrays.asList(superClass.getDeclaredFields()));
    superClass = superClass.getSuperclass();
    }


Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Enter your email address to subscribe to this blog and receive notifications of new posts by email.

Join 77 other followers

April 2010
S M T W T F S
« Mar   May »
 123
45678910
11121314151617
18192021222324
252627282930  

Blog Stats

  • 830,829 hits

%d bloggers like this: