Skip to content

Instantly share code, notes, and snippets.

@rherrmann
Last active August 29, 2022 07:10
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rherrmann/2970842 to your computer and use it in GitHub Desktop.
Save rherrmann/2970842 to your computer and use it in GitHub Desktop.
Utility class to help unit testing equals() and hashCode(), see also http://www.codeaffine.com/2012/06/25/how-do-you-test-equals-and-hashcode/
/*******************************************************************************
* Copyright (c) 2012 Rüdiger Herrmann
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Rüdiger Herrmann - initial API and implementation
* Frank Appel - code review, idea for assertEquals( T, Object, Object )
******************************************************************************/
package com.codeaffine.testhelper;
/**
* This class helps unit testing the {@link Object#equals(Object)} and {@link Object#hashCode()}
* methods.
*
* <p>The provided methods help ensuring that the requirements for an <code>equals()</code>
* implementation (<em>reflexive</em>, <em>symmetric</em>, <em>transitive</em>,
* <em>consistent</em>) and the contract between <code>equals()</code> and <code>hashCode()</code>
* are fulfilled.
* </p>
*
* <p>A typical use case for <code>EqualsTester</code> would look like below:
* </p>
* <pre>
* <code>
* {@literal @Test}
* public void testEqualsAndHashCode() {
* EqualsTester<File> tester = EqualsTester.newInstance( new File( "x" ) );
* tester.assertImplementsEqualsAndHashCode();
* tester.assertEqual( new File( "a" ), new File( "a" ) );
* tester.assertEqual( new File( "a" ), new File( "a" ), new File( "a" ) );
* tester.assertNotEqual( new File( "a" ), new File( "b" ) );
* }
* </code>
* </pre>
* @see Object#equals(Object)
* @see Object#hashCode()
*/
public class EqualsTester<T> {
/**
* Creates an instance of of EqualsTester and executes <em>general</em> tests on the given
* <code>defaultObject</code>.
*
* <p>The general tests that are done and which should apply to all implementations of
* {@link Object#equals(Object) equals()} and {@link Object#hashCode() hashCode()} are:
* </p>
* <ul>
* <li>Ensure that the object is equal with itself. I.e. <code>object.equals( object )</code>
* is true.</li>
* <li>Ensure that the object is not equal with <code>null</code>.</li>
* <li>Ensure that the object is not equal with <code>new Object()</code>.</li>
* </ul>
* @param defaultObject the object to execute the standard tests on. Must not be
* <code>null</code>.
*/
public static <T> EqualsTester<T> newInstance( T defaultObject ) {
return new EqualsTester<T>( defaultObject );
}
private final T defaultObject;
private boolean omitHashCodeTestsForUnequalPairs;
private EqualsTester( T defaultObject ) {
checkNotNull( defaultObject, "defaultObject" );
this.defaultObject = defaultObject;
assertGeneralConditions( defaultObject );
}
/**
* Prevents checking the {@link Object#hashCode() hash code} in {@link #assertNotEqual()}.
*
* <p>Once this method was called, <code>assertNotEqual()</code> does verify the hash code any
* more.
* </p>
*/
public void omitHashCodeTestForUnequalPairs() {
omitHashCodeTestsForUnequalPairs = true;
}
/**
* Ensures that the <code>defaultObject</code> that was passed to {@link #newInstance(Object)
* newInstance()} implements both {@link Object#equals(Object) equals()} and {@link
* Object#hashCode() hashCode()}.
*
* <p>It is generally necessary to override the <code>hashCode()</code> method whenever
* <code>equals()</code> is overridden, to maintain the general contract that equal objects
* must have equal hash codes. This method helps in ensuring this contract.
* </p>
*/
public void assertImplementsEqualsAndHashCode() {
new Implementation( defaultObject.getClass() ).test();
}
/**
* Ensures that the given objects and their hash code are equal.
*
* <p>The method tests if the given <code>object</code> is equal to <code>otherObject</code> and
* vice versa (is <em>symmetric</em>). Equality is determined by calling
* {@link Object#equals(Object) equals()} on the objects respectively.
* To also ensure <em>consistency</em>, the equals test is executed twice.
* </p>
* <p>If this holds true, the {@link Object#hashCode() hash code} of both objects is checked in
* that
* </p>
* <ul>
* <li>both objects must return the same hash code.</li>
* <li>it is <em>consistent</em>: return the same value when invoked more than once</li>
* </ul>
* <p>An {@link java.lang.AssertionError AssertionError} is thrown if any of the two mentioned
* conditions is not true.
* </p>
*
* @param object an object that should be equal to <code>otherObject</code>. Must not be
* <code>null</code>
* @param otherObject the object with which <code>object</code> is compared
* @throws AssertionError if <code>object</code> and <code>otherObject</code> is either not equal
* or if their hash code differs.
*/
public void assertEqual( T object, Object otherObject ) {
checkNotNull( object, "object" );
new EqualPair( object, otherObject ).test();
}
/**
* Ensures that the <code>equals()</code> implementation of the given objects is
* <em>transitive</em>.
*
* <p>This method ensures that</p>
* <ul>
* <li><code>object1.equals( object2 )</code> and</li>
* <li><code>object2.equals( object3 )</code> and</li>
* <li><code>object1.equals( object3 )</code></li>
* </ul>
* <p>An {@link java.lang.AssertionError AssertionError} is thrown if any of the equals tests
* fail.
* </p>
*
* @param object1 an object that should be equal to <code>object2</code> and <code>object3</code>.
* Must not be <code>null</code>.
* @param object2 an object that should be equal to <code>object1</code> and <code>object3</code>.
* Must not be <code>null</code>.
* @param object3 an object that should be equal to <code>object1</code> and <code>object2</code>.
* Must not be <code>null</code>.
*/
public void assertEqual( T object1, Object object2, Object object3 ) {
checkNotNull( object1, "object1" );
checkNotNull( object2, "object2" );
checkNotNull( object3, "object3" );
new EqualPair( object1, object2 ).test();
new EqualPair( object1, object3 ).test();
new EqualPair( object2, object3 ).test();
}
/**
* Ensures that the given objects are not equal. If not suppressed it is also ensured that the
* hash code of the objects differs.
*
* <p>The method tests if the given <code>object</code> is not equal to <code>otherObject</code>
* by using its {@link Object#equals(Object) equals()} method.
* Though not strictly required by the equals and {@link Object#hashCode() hash code} contract,
* it helps hash tables when the hash code of unequal objects also differs. Therefore it is
* also ensured that the hash code of both objects isn't equal.
* This test can be suppressed by calling {@link #omitHashCodeTestForUnequalPairs()} beforehand.
* </p>
* <p>An {@link java.lang.AssertionError AssertionError} is thrown if any of the above mentioned
* conditions is not true.
* </p>
*
* @param object an object that should not be equal to <code>otherObject</code>. Must not be
* <code>null</code>
* @param otherObject the object with which <code>object</code> is compared
* @throws AssertionError if <code>object</code> and <code>otherObject</code> is either equal
* or if their hash codes are the same.
*/
public void assertNotEqual( T object, Object otherObject ) {
checkNotNull( object, "object" );
new UnequalPair( object, otherObject ).test();
}
private void assertGeneralConditions( T defaultObject ) {
assertEqual( defaultObject, defaultObject );
assertNotEqual( defaultObject, null );
assertNotEqual( defaultObject, new Object() );
}
private static void checkNotNull( Object argument, String argumentName ) {
if( argument == null ) {
throw new IllegalArgumentException( "Argument must not be null: " + argumentName );
}
}
private static void assertTrue( String message, boolean condition ) {
if( !condition ) {
throw new AssertionError( message );
}
}
private static void assertFalse( String message, boolean condition ) {
if( condition ) {
throw new AssertionError( message );
}
}
static abstract class Pair {
final Object object1;
final Object object2;
Pair( Object object1, Object object2 ) {
this.object1 = object1;
this.object2 = object2;
}
void test() {
testEquals();
if( objectsNotNull() ) {
testHashCode();
}
}
abstract void testEquals();
abstract void testHashCode();
String messageForFailedEqualsTest( String messagePrefix ) {
String string1 = String.valueOf( object1 );
String string2 = String.valueOf( object2 );
return String.format( "%s for: <%s> and: <%s>", messagePrefix, string1, string2 );
}
private boolean objectsNotNull() {
return object1 != null && object2 != null;
}
}
static class EqualPair extends Pair {
EqualPair( Object object1, Object object2 ) {
super( object1, object2 );
}
@Override
void testEquals() {
String message = messageForFailedEqualsTest( "Equals test failed" );
assertTrue( message, object1.equals( object2 ) );
assertTrue( message, object1.equals( object1 ) ); // ensure consistency
assertTrue( message, object2.equals( object1 ) );
}
@Override
void testHashCode() {
boolean isEqual = object1.hashCode() == object2.hashCode();
assertTrue( messageForUnequalHashCode(), isEqual );
boolean isConsistent = object1.hashCode() == object1.hashCode();
assertTrue( messageForInconsistentHashCode(), isConsistent );
}
private String messageForUnequalHashCode() {
String message
= "HashCode is unequal for equal objects, expected: %d for <%s>, was: %d for <%s>";
String string1 = object1.toString();
String string2 = object2.toString();
Integer hashCode1 = Integer.valueOf( object1.hashCode() );
Integer hashCode2 = Integer.valueOf( object2.hashCode() );
Object[] args = { hashCode1, string1, hashCode2, string2 };
return String.format( message, args );
}
private String messageForInconsistentHashCode() {
return String.format( "HashCode is inconsistent for object: <%s>", object1.toString() );
}
}
class UnequalPair extends Pair {
UnequalPair( Object object1, Object object2 ) {
super( object1, object2 );
}
@Override
void testEquals() {
String message = messageForFailedEqualsTest( "Unequals test failed" );
assertFalse( message, object1.equals( object2 ) );
}
@Override
void testHashCode() {
if( !omitHashCodeTestsForUnequalPairs ) {
String msg = messageForFailedHashCodeTest();
assertTrue( msg, object1.hashCode() != object2.hashCode() );
}
}
private String messageForFailedHashCodeTest() {
String message
= "HashCode test failed for unequal objects, was %d for: <%s> and for: <%s>";
String string1 = object1.toString();
String string2 = object2.toString();
Integer hashCode = Integer.valueOf( object1.hashCode() );
Object[] args = { hashCode, string1, string2 };
return String.format( message, args );
}
}
static class Implementation {
private final Class<? extends Object> type;
Implementation( Class<? extends Object> type ) {
this.type = type;
}
void test() {
String message = messageForUnimplementedEqualsAndHashCode();
assertTrue( message, declaresEquals() && declaresHashCode() );
}
private boolean declaresEquals() {
return declaresMethod( "equals", new Class[] { Object.class } );
}
private boolean declaresHashCode() {
return declaresMethod( "hashCode", ( Class<?>[] )null );
}
private boolean declaresMethod( String methodName, Class<?>... parameters ) {
boolean result = false;
try {
type.getDeclaredMethod( methodName, parameters );
result = true;
} catch( SecurityException ignore ) {
} catch( NoSuchMethodException ignore ) {
}
return result;
}
private String messageForUnimplementedEqualsAndHashCode() {
String className = type.getSimpleName();
return String.format( "%s does not implement both, equals() and hashCode()", className );
}
}
}
@hstaudacher
Copy link

Thanks Rüdiger for this useful tool. I embedded it in a project and it works like a charm ;)

But there is one annoying thing. The EqualsTester provokes 2 Findbugs warnings when embedding it in a build. They are harmless but annoying... I forked your gist and removed the warnings, see https://gist.github.com/3003719

Maybe you would like to fetch the 3 line changes.... ;)

Cheers Holger

@rherrmann
Copy link
Author

Hi Holger, I'm glad you found it useful.
Your changes are merged.

Thanks, Rüdiger

@vivekach
Copy link

Hi Rüdiger

Thank you so much.. It is really useful library/tool.

Vivek Acharya

@soumyadey
Copy link

Thanks for this utility!

I am keeping this class in a testutils module for reuse across other modules. This class being reasonably large brings down code coverage a bit.

Do you have accompanying unit tests for this class? I could reuse if someone has written tests already!

Thanks!

@rherrmann
Copy link
Author

@soumyadey I'm afraid, there aren't dedicated tests. This code was extracted from existing tests and utility code, where the existing production- and test-code served as a reference.

Rüdiger

@soumyadey
Copy link

Okay, thanks Rüdiger. Meanwhile I found an alternative in EqualsTester from guava-testlib. I was already using guava in my project, but was not aware of this package.

new EqualsTester()
	.addEqualityGroup(new Name("first", "middle", "last"), new Name("first", "last"))
	.addEqualityGroup(new Name(null, "last"))
	.addEqualityGroup(new Name("first", null))
	.addEqualityGroup(new Name(null, null, null))
	.testEquals();

Each group should contain objects that are equal to each other but unequal to the objects in any other group.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment