Last active
February 25, 2022 19:08
-
-
Save nniesen/d87eb6db40a6fb28de525ec1819789c1 to your computer and use it in GitHub Desktop.
Assertion method for validating equals/HashCode implementations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.test; | |
import static org.junit.Assert.assertFalse; | |
import static org.junit.Assert.assertNotSame; | |
import static org.junit.Assert.assertTrue; | |
public class AssertEqualsHashCode { | |
/** | |
* Assert that the equals method is reflexive, symmetric, transitive, consistent, and should return a false if the reference | |
* argument is null and asserts that the hashCode method is consistent with equals (i.e., if objects are equal their hash | |
* codes must be equal). | |
* <p> | |
* Testing notes: | |
* <ul> | |
* <li>Call this method once for each field value (permutation) needed to cover all code paths that effect equality and hash | |
* code values.</li> | |
* <li>For objects where multiple fields determine equality, test each field making sure to only permutate one field value at | |
* a time.</li> | |
* <li>Hash code tests are only performed using the equal instances so be sure to test any needed field permutations for the | |
* equal instances.</li> | |
* </ul> | |
* Definitions (Follows the contract Javadoc for equals and hashCode): | |
* <ul> | |
* <li>Reflexivity requires that for any reference x, x.equals(x) should return true.</li> | |
* <li>Symmetry requires that for any references x and y, x.equals(y) should return true if and only if y.equals(x) returns | |
* true.</li> | |
* <li>Transitivity requires that for any references x, y and z, if x.equals(y) returns true and y.equals(z) returns true, | |
* then x.equals(z) should return true.</li> | |
* <li>Consistency requires that repeated calls to x.equals(y) should consistently return a true or consistently return a | |
* false, if no data /state has changed in either object.</li> | |
* <li>Non-nullity requires that the for any non-null reference x, x.equals(null) should return false.</li> | |
* </ul> | |
* | |
* @param messagePrefix A prefix, such as the field being permutated, to apply to the {@code AssertionError} messages. The | |
* prefix should include whitespace as none will be added. | |
* @param x An instance considered to be equal. | |
* @param y An instance considered to be equal. | |
* @param z An instance considered to be equal. | |
* @param n An instance considered to be NOT equal. | |
*/ | |
public static void assertEqualsHashCode(final String messagePrefix, final Object x, final Object y, final Object z, | |
final Object n) { | |
// All instances must be unique. | |
assertNotSame(messagePrefix + "Expected unique: x ! y", x, y); | |
assertNotSame(messagePrefix + "Expected unique: y ! z", y, z); | |
assertNotSame(messagePrefix + "Expected unique: z ! x", z, x); | |
assertNotSame(messagePrefix + "Expected unique: x ! n", x, n); | |
assertNotSame(messagePrefix + "Expected unique: y ! n", y, n); | |
assertNotSame(messagePrefix + "Expected unique: z ! n", z, n); | |
assertTrue(messagePrefix + "Expected reflexive equals: x.equals(x)", x.equals(x)); | |
assertTrue(messagePrefix + "Expected consistent equals: x.equals(x)", x.equals(x)); | |
assertFalse(messagePrefix + "Expected not equal null: x!=null", x.equals(null)); | |
assertFalse(messagePrefix + "Expected not equal other class: x!=other", x.equals(new BaseTest() { | |
})); | |
assertTrue(messagePrefix + "Expected symmetric equals: x=y", x.equals(y)); | |
assertTrue(messagePrefix + "Expected symmetric equals: y=x", y.equals(x)); | |
// given xy are symmetric... | |
assertTrue(messagePrefix + "Expected transitive equals: y=z", y.equals(z)); | |
assertTrue(messagePrefix + "Expected transitive equals: x=z", x.equals(z)); | |
assertFalse(messagePrefix + "Expected not equal: x!=n", x.equals(n)); | |
assertFalse(messagePrefix + "Expected not equal: n!=x", n.equals(x)); | |
// If equal, hash codes must be equal. Object n is not tested because hash codes do not have to be unique, just consistent with equals. | |
assertTrue(messagePrefix + "Expected hash code equals: x==y", x.hashCode() == y.hashCode()); | |
assertTrue(messagePrefix + "Expected hash code equals: y==z", y.hashCode() == z.hashCode()); | |
assertTrue(messagePrefix + "Expected hash code equals: z==x", z.hashCode() == x.hashCode()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.test; | |
import static org.junit.Assert.assertEquals; | |
import org.junit.Test; | |
public class AssertEqualsHashCodeTest { | |
@Test | |
public void testAssertEqualsHashCode_UniqueInstance() { | |
TestSingleFieldEquals x = new TestSingleFieldEquals("A"); | |
TestSingleFieldEquals y = new TestSingleFieldEquals("A"); | |
TestSingleFieldEquals z = new TestSingleFieldEquals("A"); | |
TestSingleFieldEquals n = new TestSingleFieldEquals("B"); | |
assertUniqueEqualsHashCodeInstances(x, x, z, n, "x ! y"); | |
assertUniqueEqualsHashCodeInstances(x, y, y, n, "y ! z"); | |
assertUniqueEqualsHashCodeInstances(z, y, z, n, "z ! x"); | |
assertUniqueEqualsHashCodeInstances(x, y, z, x, "x ! n"); | |
assertUniqueEqualsHashCodeInstances(x, y, z, y, "y ! n"); | |
assertUniqueEqualsHashCodeInstances(x, y, z, z, "z ! n"); | |
} | |
private void assertUniqueEqualsHashCodeInstances(final TestSingleFieldEquals x, final TestSingleFieldEquals y, | |
final TestSingleFieldEquals z, final TestSingleFieldEquals n, final String expectedMessage) { | |
try { | |
assertEqualsHashCode(x, y, z, n); | |
fail("Expected AssertionError with message: " + expectedMessage); | |
} catch (AssertionError e) { | |
assertEquals("Expected unique: " + expectedMessage + " expected not same", e.getMessage()); | |
} | |
} | |
@Test | |
public void testAssertEqualsHashCode_NotEqualInstanceIsEqual() { | |
this.thrown.expect(AssertionError.class); | |
this.thrown.expectMessage("Expected not equal: x!=n"); | |
TestSingleFieldEquals x = new TestSingleFieldEquals("A"); | |
TestSingleFieldEquals y = new TestSingleFieldEquals("A"); | |
TestSingleFieldEquals z = new TestSingleFieldEquals("A"); | |
TestSingleFieldEquals n = new TestSingleFieldEquals("A"); | |
assertEqualsHashCode(x, y, z, n); | |
} | |
/** | |
* Example usage of assertEqualsHashCode for testing class with one field participating in Equals and HashCode. | |
*/ | |
@Test | |
public void testAssertEqualsHashCode_SingleField() { | |
TestSingleFieldEquals x = new TestSingleFieldEquals("SameValue"); | |
TestSingleFieldEquals y = new TestSingleFieldEquals("SameValue"); | |
TestSingleFieldEquals z = new TestSingleFieldEquals("SameValue"); | |
// Test a not-equal value for the field | |
TestSingleFieldEquals n = new TestSingleFieldEquals("DifferentValue"); | |
assertEqualsHashCode(x, y, z, n); | |
// Test any special values for the field that affect equality. | |
n = new TestSingleFieldEquals(/* empty char[] */); | |
assertEqualsHashCode(x, y, z, n); | |
// Also test special empty value on the equal instances for hash codes. | |
x = new TestSingleFieldEquals(/* empty char[] */); | |
y = new TestSingleFieldEquals(/* empty char[] */); | |
z = new TestSingleFieldEquals(/* empty char[] */); | |
n = new TestSingleFieldEquals("NotEmpty"); | |
assertEqualsHashCode(x, y, z, n); | |
// Test special value where length affects code path. | |
x = new TestSingleFieldEquals("SameLengthAndSameValue"); | |
y = new TestSingleFieldEquals("SameLengthAndSameValue"); | |
z = new TestSingleFieldEquals("SameLengthAndSameValue"); | |
n = new TestSingleFieldEquals("SameLengthButDiffValue"); | |
assertEqualsHashCode(x, y, z, n); | |
} | |
private static class TestSingleFieldEquals { | |
private final char value[]; | |
public TestSingleFieldEquals() { | |
this.value = new char[0]; // <- interesting field value, empty array | |
} | |
public TestSingleFieldEquals(final String original) { | |
this.value = original.toCharArray(); | |
} | |
@Override | |
public int hashCode() { | |
int result = 0; | |
if (this.value.length > 0) { // <- Note: need to test empty permutation for the equal instances. | |
char val[] = this.value; | |
for (int i = 0; i < this.value.length; i++) { | |
result = (31 * result) + val[i]; | |
} | |
} | |
return result; | |
} | |
@Override | |
public boolean equals(final Object obj) { | |
if (this == obj) { | |
return true; | |
} | |
if (obj instanceof TestSingleFieldEquals) { | |
TestSingleFieldEquals other = (TestSingleFieldEquals) obj; | |
int n = this.value.length; | |
if (n == other.value.length) { | |
char v1[] = this.value; | |
char v2[] = other.value; | |
int i = 0; | |
while (n-- != 0) { | |
if (v1[i] != v2[i]) { | |
return false; // <- Note: need to test permutation where values are same length but different. | |
} | |
i++; | |
} | |
return true; | |
} | |
} | |
return false; | |
} | |
} | |
/** | |
* Example usage of assertEqualsHashCode for testing class with multiple fields participating in Equals and HashCode. | |
*/ | |
@Test | |
public void testAssertEqualsHashCode_MultipleFields() { | |
TestMultiFieldEquals x = new TestMultiFieldEquals("Same", 75); | |
TestMultiFieldEquals y = new TestMultiFieldEquals("Same", 75); | |
TestMultiFieldEquals z = new TestMultiFieldEquals("Same", 75); | |
// For objects where multiple fields determine equality, test each field and only permutate one at a time. | |
// Test change in name field | |
TestMultiFieldEquals n = new TestMultiFieldEquals("Diff", 75); | |
assertEqualsHashCode("Permutated name - ", x, y, z, n); | |
// Test change in age field | |
n = new TestMultiFieldEquals("Same", 35); | |
assertEqualsHashCode("Permutated age - ", x, y, z, n); | |
// Test for special field properties: e.g., null for name field | |
n = new TestMultiFieldEquals(null, 75); | |
assertEqualsHashCode("Null name - ", x, y, z, n); | |
// Also test for special field properties where a different code path is taken if the special value is on | |
// the "equals" objects. This is also needed to fully test hash codes with special field properties since | |
// hash code tests are only performed on the "equals" objects. | |
x = new TestMultiFieldEquals(null, 75); | |
y = new TestMultiFieldEquals(null, 75); | |
z = new TestMultiFieldEquals(null, 75); | |
n = new TestMultiFieldEquals("NotNull", 75); | |
assertEqualsHashCode("Null name (on equals instances) - ", x, y, z, n); | |
} | |
private static class TestMultiFieldEquals { | |
private final String name; | |
private final int age; | |
public TestMultiFieldEquals(final String name, final int age) { | |
this.name = name; | |
this.age = age; | |
} | |
@Override | |
public int hashCode() { | |
final int prime = 31; | |
int result = 1; | |
result = (prime * result) + ((this.name == null) ? 0 : this.name.hashCode()); // <- Note: need to test null permutation for the equal instances. | |
result = (prime * result) + this.age; | |
return result; | |
} | |
@Override | |
public boolean equals(final Object obj) { | |
if (this == obj) { | |
return true; | |
} | |
if (obj == null) { | |
return false; | |
} | |
if (getClass() != obj.getClass()) { | |
return false; | |
} | |
TestMultiFieldEquals other = (TestMultiFieldEquals) obj; | |
if (this.name == null) { // <-------- Note: need to test null permutation for both equal and not-equal instances. | |
if (other.name != null) { // <-/ | |
return false; | |
} | |
} else if (!this.name.equals(other.name)) { | |
return false; | |
} | |
if (this.age != other.age) { | |
return false; | |
} | |
return true; | |
} | |
} | |
@Test | |
public void testAssertEqualsHashCode_ImplementationFlaw_NotReflexive() { | |
EqualsHashCodeImpl x = new EqualsHashCodeImpl("A", 1); | |
EqualsHashCodeImpl y = new EqualsHashCodeImpl("A", 2); | |
EqualsHashCodeImpl z = new EqualsHashCodeImpl("A", 3); | |
EqualsHashCodeImpl n = new EqualsHashCodeImpl("B", 1); | |
assertImplementationFlaw(x, y, z, n, "notReflexive", "Expected reflexive equals: x.equals(x)"); | |
} | |
@Test | |
public void testAssertEqualsHashCode_ImplementationFlaw_NotConsistent() { | |
EqualsHashCodeImpl x = new EqualsHashCodeImpl("A", 1); | |
EqualsHashCodeImpl y = new EqualsHashCodeImpl("A", 2); | |
EqualsHashCodeImpl z = new EqualsHashCodeImpl("A", 3); | |
EqualsHashCodeImpl n = new EqualsHashCodeImpl("B", 1); | |
assertImplementationFlaw(x, y, z, n, "notConsistent", "Expected consistent equals: x.equals(x)"); | |
} | |
@Test | |
public void testAssertEqualsHashCode_ImplementationFlaw_EqualsNull() { | |
EqualsHashCodeImpl x = new EqualsHashCodeImpl("A", 1); | |
EqualsHashCodeImpl y = new EqualsHashCodeImpl("A", 2); | |
EqualsHashCodeImpl z = new EqualsHashCodeImpl("A", 3); | |
EqualsHashCodeImpl n = new EqualsHashCodeImpl("B", 1); | |
assertImplementationFlaw(x, y, z, n, "equalsNull", "Expected not equal null: x!=null"); | |
} | |
@Test | |
public void testAssertEqualsHashCode_ImplementationFlaw_EqualsAnotherClass() { | |
EqualsHashCodeImpl x = new EqualsHashCodeImpl("A", 1); | |
EqualsHashCodeImpl y = new EqualsHashCodeImpl("A", 2); | |
EqualsHashCodeImpl z = new EqualsHashCodeImpl("A", 3); | |
EqualsHashCodeImpl n = new EqualsHashCodeImpl("B", 1); | |
assertImplementationFlaw(x, y, z, n, "equalsAnotherClass", "Expected not equal other class: x!=other"); | |
} | |
@Test | |
public void testAssertEqualsHashCode_ImplementationFlaw_NotSymmetric() { | |
EqualsHashCodeImpl x = new EqualsHashCodeImpl(null, 1); | |
EqualsHashCodeImpl y = new EqualsHashCodeImpl(null, 2); | |
EqualsHashCodeImpl z = new EqualsHashCodeImpl(null, 3); | |
EqualsHashCodeImpl n = new EqualsHashCodeImpl("B", 1); | |
assertImplementationFlaw(x, y, z, n, "notSymmetric", "Expected symmetric equals: x=y"); | |
} | |
@Test | |
public void testAssertEqualsHashCode_ImplementationFlaw_NotTransitive() { | |
EqualsHashCodeImpl x = new EqualsHashCodeImpl("A", 1); | |
EqualsHashCodeImpl y = new EqualsHashCodeImpl("A", 1); | |
EqualsHashCodeImpl z = new EqualsHashCodeImpl("A", 3); | |
EqualsHashCodeImpl n = new EqualsHashCodeImpl("B", 1); | |
assertImplementationFlaw(x, y, z, n, "notTransitive", "Expected transitive equals: y=z"); | |
} | |
@Test | |
public void testAssertEqualsHashCode_ImplementationFlaw_HashCodeInconsistentWithEquals() { | |
EqualsHashCodeImpl x = new EqualsHashCodeImpl("A", 1); | |
EqualsHashCodeImpl y = new EqualsHashCodeImpl("A", 2); | |
EqualsHashCodeImpl z = new EqualsHashCodeImpl("A", 3); | |
EqualsHashCodeImpl n = new EqualsHashCodeImpl("B", 1); | |
assertImplementationFlaw(x, y, z, n, "inconsistentHashCode", "Expected hash code equals: x==y"); | |
} | |
private void assertImplementationFlaw(final EqualsHashCodeImpl x, final EqualsHashCodeImpl y, final EqualsHashCodeImpl z, | |
final EqualsHashCodeImpl n, final String implementationFlaw, final String expectMessage) { | |
// Assert success without flaw | |
EqualsHashCodeImpl.setImplementationFlaw("none"); | |
assertEqualsHashCode(x, y, z, n); | |
// Assert failure with flaw | |
this.thrown.expect(AssertionError.class); | |
this.thrown.expectMessage(expectMessage); | |
EqualsHashCodeImpl.setImplementationFlaw(implementationFlaw); | |
assertEqualsHashCode(x, y, z, n); | |
} | |
private static class EqualsHashCodeImpl { | |
protected static int callCount = 0; | |
protected static boolean inconsistentHashCode; | |
protected static boolean notReflexive; | |
protected static boolean notConsistent; | |
protected static boolean equalsNull; | |
protected static boolean equalsAnotherClass; | |
protected static boolean notTransitive; | |
protected static boolean notSymmetric; | |
private final String fieldA; | |
private final int fieldB; | |
public EqualsHashCodeImpl(final String fieldA, final int fieldB) { | |
this.fieldA = fieldA; | |
this.fieldB = fieldB; | |
} | |
public static void setImplementationFlaw(final String implementationFlaw) { | |
callCount = 0; | |
inconsistentHashCode = false; | |
notReflexive = false; | |
notConsistent = false; | |
equalsNull = false; | |
equalsAnotherClass = false; | |
notTransitive = false; | |
notSymmetric = false; | |
if (!"none".equals(implementationFlaw)) { | |
setFieldValue(EqualsHashCodeImpl.class, implementationFlaw, Boolean.TRUE); | |
} | |
} | |
public static void setFieldValue(final Class<?> clazz, final String name, final Object value) { | |
try { | |
Field field = clazz.getDeclaredField(name); | |
field.setAccessible(true); | |
field.set(clazz, value); | |
} catch (final Exception e) { | |
throw new RuntimeException(e); | |
} | |
} | |
@Override | |
public int hashCode() { | |
final int prime = 31; | |
int result = 1; | |
result = (prime * result) + ((this.fieldA == null) ? 0 : this.fieldA.hashCode()); | |
if (inconsistentHashCode) { | |
result = (prime * result) + this.fieldB; // inconsistent, add fieldB which is not part of equals | |
} | |
return result; | |
} | |
@Override | |
public boolean equals(final Object obj) { | |
if (this == obj) { | |
if (notReflexive) { | |
return false; // not reflexive, return false for same obj | |
} | |
if (notConsistent) { | |
callCount++; | |
return callCount < 2; // not consistent, change the result on subsequent call | |
} | |
return true; | |
} | |
if (obj == null) { | |
if (equalsNull) { | |
return true; // equals null, return true for null obj | |
} | |
return false; | |
} | |
if (getClass() != obj.getClass()) { | |
if (equalsAnotherClass) { | |
return true; // equals another class, return true for obj of another class | |
} | |
return false; | |
} | |
EqualsHashCodeImpl other = (EqualsHashCodeImpl) obj; | |
if (notTransitive) { | |
if (this.fieldB != other.fieldB) { | |
return false; // not transitive, add fieldB check that shouldn't be part of equals | |
} | |
} | |
if (this.fieldA == null) { | |
if (notSymmetric) { | |
return false; // not symmetric, skip check of other != null | |
} | |
if (other.fieldA != null) { | |
return false; | |
} | |
} else if (!this.fieldA.equals(other.fieldA)) { | |
return false; | |
} | |
return true; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment