View Javadoc

1   /*
2    * Copyright (c) 1998, 2005 Gargoyle Software Inc. All rights reserved.
3    *
4    * Redistribution and use in source and binary forms, with or without
5    * modification, are permitted provided that the following conditions are met:
6    *
7    * 1. Redistributions of source code must retain the above copyright notice,
8    *    this list of conditions and the following disclaimer.
9    * 2. Redistributions in binary form must reproduce the above copyright notice,
10   *    this list of conditions and the following disclaimer in the documentation
11   *    and/or other materials provided with the distribution.
12   * 3. The end-user documentation included with the redistribution, if any, must
13   *    include the following acknowledgment:
14   *
15   *       "This product includes software developed by Gargoyle Software Inc.
16   *        (http://www.GargoyleSoftware.com/)."
17   *
18   *    Alternately, this acknowledgment may appear in the software itself, if
19   *    and wherever such third-party acknowledgments normally appear.
20   * 4. The name "Gargoyle Software" must not be used to endorse or promote
21   *    products derived from this software without prior written permission.
22   *    For written permission, please contact info@GargoyleSoftware.com.
23   * 5. Products derived from this software may not be called "GSBase", nor may
24   *    "GSBase" appear in their name, without prior written permission of
25   *    Gargoyle Software Inc.
26   *
27   * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
28   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
29   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARGOYLE
30   * SOFTWARE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
31   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
32   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
33   * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
34   * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
35   * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
36   * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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