1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 package com.gargoylesoftware.base.testing;
39
40 import com.gargoylesoftware.base.util.DetailedNullPointerException;
41 import java.lang.reflect.Constructor;
42 import java.lang.reflect.Method;
43 import java.lang.reflect.Modifier;
44 import junit.framework.Assert;
45
46 /***
47 * EqualsTester is used to test the equals contract on objects. The contract as
48 * specified by java.lang.Object states that if A.equals(B) is true then B.equals(A)
49 * is also true. It also specifies that if A.equals(B) is true then A.hashCode()
50 * will equals B.hashCode().<p>
51 *
52 * It is also common practice to implement equals using an instanceof check which
53 * will result in false positives in some cases. Specifically, it will result in
54 * false positives when comparing against a subclass with the same values. For an
55 * in-depth discussion of the common problems when implementing the equals contract,
56 * refer to the book "Practical Java" by Peter Haggar
57 * <pre>
58 * // WRONG way of implementing equals
59 * public boolean equals( final Object object ) {
60 * if( object instanceof this ) {
61 * // do check
62 * }
63 * return false;
64 * }
65 * </pre>
66 *
67 * The correct way to implement equals is as follows
68 * <pre>
69 * public boolean equals( final Object object ) {
70 * if( object != null && object.getClass() == this.getClass() ) {
71 * // do check
72 * }
73 * return false;
74 * }
75 * </pre>
76 *
77 * EqualsTester ensures that the equals() and hashCode() methods have been
78 * implemented correctly.<p>
79 *
80 * <pre>
81 * final Object a = new Foo(4); // original object
82 * final Object b = new Foo(4); // another object that has the same values as the original
83 * final Object c = new Foo(5); // another object with different values
84 * final Object d = new Foo(4) {}; // a subclass of Foo with the same values as the original
85 * new EqualsTester(a, b, c, d);
86 * </pre>
87 *
88 * @version $Revision: 1.5 $
89 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
90 */
91 public class EqualsTester extends Assert {
92
93 /***
94 * Perform the test. The act of instantiating one of these will run the test.
95 *
96 * @param a The object to be tested
97 * @param b An object that is equal to A
98 * @param c An object of the same class that is not equal to A. If it is
99 * not possible to create a different one then pass null.
100 * @param d A subclass of A with the same values. If A is an instance of a
101 * final class then this must be null
102 */
103 public EqualsTester( final Object a, final Object b, final Object c, final Object d ) {
104 assertNotNull( a, "A" );
105 assertNotNull( b, "B" );
106 assertSameClassAsA( a, b, "B" );
107
108 if( c == null ) {
109 assertCAllowedToBeNull( a.getClass() );
110 }
111 else {
112 assertSameClassAsA( a, c, "C" );
113 }
114
115 if( isClassFinal( a.getClass() ) ) {
116 assertNull( d, "D" );
117 }
118 else if( d == null ) {
119 throw new DetailedNullPointerException( "D", "Cannot be null for a non-final class" );
120 }
121
122 if( d != null ) {
123 assertDDifferentClassThanA( a, d );
124 }
125
126 assertAEqualsNull( a );
127 assertAEqualsA( a );
128 assertAEqualsB( a, b );
129 if( c != null ) {
130 assertANotEqualC( a, c );
131 }
132 assertClassAndSubclass( a, d );
133 }
134
135
136 private boolean isClassFinal( final Class clazz ) {
137 final int modifiers = clazz.getModifiers();
138 return Modifier.isFinal( modifiers );
139 }
140
141
142 private void assertAEqualsA( final Object a ) {
143 assertTrue( "A.equals(A)", a.equals( a ) );
144 }
145
146
147 private void assertAEqualsB( final Object a, final Object b ) {
148 assertTrue( "A.equals(B)", a.equals( b ) );
149 assertTrue( "B.equals(A)", b.equals( a ) );
150 assertEquals( "hashCode", a.hashCode(), b.hashCode() );
151 }
152
153
154 private void assertANotEqualC( final Object a, final Object c ) {
155 assertTrue( "a.equals(c)", a.equals( c ) == false );
156 assertTrue( "c.equals(a)", c.equals( a ) == false );
157 }
158
159
160 private void assertClassAndSubclass( final Object a, final Object d ) {
161 if( d != null ) {
162 if( a.equals( d ) == true ) {
163 fail( "a.equals(d)" );
164 }
165
166 if( d.equals( a ) == true ) {
167 fail( "d.equals(a)" );
168 }
169 }
170 }
171
172
173
174 private void assertNotNull( final Object object, final String description ) {
175 if( object == null ) {
176 fail( description + " is null" );
177 }
178 }
179
180
181 private void assertNull( final Object object, final String description ) {
182 if( object != null ) {
183 fail( description + " must be null. Found [" + object + "]" );
184 }
185 }
186
187
188 /***
189 * C may not be null if it has a public non-default constructor or any
190 * setXX() methods
191 *
192 * @param clazz
193 */
194 private void assertCAllowedToBeNull( final Class clazz ) {
195 int i;
196
197 final Constructor constructors[] = clazz.getConstructors();
198 for( i = 0; i < constructors.length; i++ ) {
199 if( constructors[i].getParameterTypes().length != 0 ) {
200 fail( "C may not be null because it has a public non-default constructor: " + constructors[i] );
201 }
202 }
203
204 final Method methods[] = clazz.getMethods();
205 for( i = 0; i < methods.length; i++ ) {
206 if( methods[i].getName().startsWith( "set" ) ) {
207 fail( "C may not be null because it has public set methods: " + methods[i] );
208 }
209 }
210 }
211
212
213 private void assertSameClassAsA( final Object a, final Object object, final String name ) {
214 if( a.getClass() != object.getClass() ) {
215 fail(
216 name
217 + " must be of same class as A. A.class=["
218 + a.getClass().getName()
219 + "] "
220 + name
221 + ".class=["
222 + object.getClass().getName()
223 + "]" );
224 }
225 }
226
227
228 private void assertDDifferentClassThanA( final Object a, final Object d ) {
229 if( a.getClass() == d.getClass() ) {
230 fail(
231 "D must not be of same class as A. A.class=["
232 + a.getClass().getName()
233 + "] d.class=["
234 + d.getClass().getName()
235 + "]" );
236 }
237 }
238
239
240 private void assertAEqualsNull( final Object a ) {
241 try {
242 if( a.equals( null ) == true ) {
243 fail( "A.equals(null) returned true" );
244 }
245 }
246 catch( final NullPointerException e ) {
247 fail( "a.equals(null) threw a NullPointerException. It should have returned false" );
248 }
249 }
250 }
251