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.
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