1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.outsideMyBox.testUtils;
17
18 import java.lang.reflect.Array;
19 import java.lang.reflect.Constructor;
20 import java.lang.reflect.Method;
21 import java.text.MessageFormat;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Map.Entry;
30 import java.util.Set;
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 public final class BeanLikeTester {
48
49
50
51
52 public static final class PropertiesAndValues extends HashMap<String, Object> {
53 private static final long serialVersionUID = 1L;
54
55 public PropertiesAndValues(PropertiesAndValues defaultValues) {
56 super(defaultValues);
57 }
58
59 public PropertiesAndValues() {
60 super();
61 }
62 }
63
64
65
66
67
68
69 public static final class ConstructorSignatureAndPropertiesMapping extends HashMap<List<Class<?>>, List<String>> {
70 private static final long serialVersionUID = 1L;
71 }
72
73 private static final ConstructorSignatureAndPropertiesMapping NOARG_SIGNATUREANDPROPS = new ConstructorSignatureAndPropertiesMapping();
74
75 static {
76 NOARG_SIGNATUREANDPROPS.put(Collections.<Class<?>> emptyList(), Collections.<String> emptyList());
77 }
78
79 private static final class AClassToBeTestedAgainst {
80 private static AClassToBeTestedAgainst instance = new AClassToBeTestedAgainst();
81
82 private AClassToBeTestedAgainst() {
83 }
84 }
85
86
87
88 private final Class<?> beanLikeClass;
89 private final ConstructorSignatureAndPropertiesMapping constructorsSignaturesAndProperties;
90 private final Set<String> gettablePropertyNames;
91 private final Set<String> settablePropertyNames;
92 private final Set<String> mutablePropertyNames;
93 private final Map<String, Method> accessors;
94 private final Map<String, Method> setters;
95
96
97
98
99
100
101
102
103
104
105
106
107 public BeanLikeTester(Class<?> beanLikeClass, ConstructorSignatureAndPropertiesMapping constructorsSignaturesAndProperties) {
108 this.beanLikeClass = beanLikeClass;
109 this.constructorsSignaturesAndProperties = constructorsSignaturesAndProperties == null ? NOARG_SIGNATUREANDPROPS : constructorsSignaturesAndProperties;
110 accessors = getAccessors();
111 setters = getSetters();
112 gettablePropertyNames = accessors.keySet();
113 settablePropertyNames = setters.keySet();
114 mutablePropertyNames = getMutableProperyNames();
115 verifyConstructorSignaturesMatchSignaturesFromArgs();
116 verifySettersAndAccessorsAreValid();
117 verifyAllPropertiesHaveAnAccessor();
118 }
119
120
121
122
123
124
125 public BeanLikeTester(Class<?> beanClass) {
126 this(beanClass, null);
127 }
128
129
130
131
132
133
134
135 private Set<String> getMutableProperyNames() {
136 final Set<String> settableProperties = new HashSet<String>();
137 for (final List<String> props : constructorsSignaturesAndProperties.values()) {
138 settableProperties.addAll(props);
139 }
140 settableProperties.addAll(settablePropertyNames);
141 return settableProperties;
142 }
143
144
145
146
147
148 private void verifyConstructorSignaturesMatchSignaturesFromArgs() {
149 final Set<List<Class<?>>> signaturesFromArgs = constructorsSignaturesAndProperties.keySet();
150 final Set<List<Class<?>>> signaturesFromConstructor = new HashSet<List<Class<?>>>();
151
152 for (final Constructor<?> constructor : beanLikeClass.getConstructors()) {
153 final List<Class<?>> signature = Arrays.asList(constructor.getParameterTypes());
154 signaturesFromConstructor.add(signature);
155
156 }
157 if (!signaturesFromArgs.equals(signaturesFromConstructor)) {
158 throw new BeanLikeTesterException("The signatures from the constructor's argument must be the same as the bean:\nFrom args: " + signaturesFromArgs
159 + "\nFrom object:" + signaturesFromConstructor);
160 }
161 }
162
163
164
165
166
167 private void verifySettersAndAccessorsAreValid() {
168 final Method[] methods = beanLikeClass.getMethods();
169
170 for (final Method method : methods) {
171 final String methodName = method.getName();
172 if (isSetter(method)) {
173 throw new BeanLikeTesterException("The method '" + methodName + "' must not return an object.");
174 }
175 if (isIsGetter(method)) {
176 throw new BeanLikeTesterException("The method '" + methodName + "' doesn't return a boolean");
177 }
178 else if (isGetGetter(method)) {
179 throw new BeanLikeTesterException("The method '" + methodName + "' doesn't return an object");
180 }
181 }
182 }
183
184 private boolean isSetter(Method method) {
185 return method.getName().startsWith("set") && !method.getReturnType().equals(Void.TYPE);
186 }
187
188 private boolean isIsGetter(Method method) {
189 return method.getName().startsWith("is") && !method.getReturnType().equals(Boolean.class) && !method.getReturnType().equals(boolean.class);
190 }
191
192 private boolean isGetGetter(Method method) {
193 return method.getName().startsWith("get") && !method.getName().equals("getClass") && (method.getParameterTypes().length == 0) && method.getReturnType().equals(Void.TYPE);
194 }
195
196
197
198
199
200 private void verifyAllPropertiesHaveAnAccessor() {
201 final Set<String> nonAccessibleProperties = new HashSet<String>(mutablePropertyNames);
202 nonAccessibleProperties.removeAll(gettablePropertyNames);
203 if (!nonAccessibleProperties.isEmpty()) {
204 throw new BeanLikeTesterException("The following properties don't have any accessor:" + nonAccessibleProperties);
205 }
206 }
207
208
209
210
211
212
213
214
215
216 private static Object createNewInstance(String fullyQualifiedClassName, Class<?>[] constructorSignature, Object... constructorParams) {
217 try {
218 final Class<?> classToInstantiate = Class.forName(fullyQualifiedClassName);
219 final Constructor<? extends Object> classConstructor = classToInstantiate.getConstructor(constructorSignature);
220 return classConstructor.newInstance(constructorParams);
221 } catch (final Exception e) {
222 final String msg = MessageFormat.format("exception msg: {0} \n\tfullyQualifiedClassName: {1} \n\tconstructorSignature: {2} \n\tconstructorParams: {3}",
223 e,
224 fullyQualifiedClassName,
225 Arrays.asList(constructorSignature),
226 Arrays.asList(constructorParams));
227 throw new BeanLikeTesterException(msg, e);
228 }
229 }
230
231
232
233
234
235
236
237
238
239 private static Object invokeMethod(Object object, Method method, Object... args) {
240 try {
241 return method.invoke(object, args);
242 } catch (final Exception e) {
243 throw new BeanLikeTesterException(e.getMessage(), e);
244 }
245 }
246
247 private static String getPropertyNameFromMethodName(String methodName) {
248 final String propertyName = methodName.replaceFirst("^is|set|get", "");
249 return Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1);
250 }
251
252 private static Object[] createArrayFromArrayObject(Object o) {
253 if(!o.getClass().getComponentType().isPrimitive())
254 return (Object[])o;
255
256 int arrayLength = Array.getLength(o);
257 Object elements[] = new Object[arrayLength];
258 for(int i = 0; i < arrayLength; i++){
259 elements[i] = Array.get(o, i);
260 }
261 return elements;
262 }
263
264 private static boolean areValuesDifferent(Object value1, Object value2) {
265 if ((value1 !=null) && (value2 != null) && (value1.getClass().isArray()) && (value2.getClass().isArray())) {
266 final Object[] array1 = createArrayFromArrayObject(value1);
267 final Object[] array2 = createArrayFromArrayObject(value2);
268 return !Arrays.deepEquals(array1, array2);
269 }
270 final boolean conditionToFail1 = (value1 != null) && ((value2 == null) || !value1.equals(value2));
271 final boolean conditionToFail2 = (value2 != null) && ((value1 == null) || !value2.equals(value1));
272 return conditionToFail1 || conditionToFail2;
273 }
274
275
276
277
278
279
280
281
282 private void setProperty(Object beanLike, String propertyName, Object value) {
283 final Method setter = setters.get(propertyName);
284 invokeMethod(beanLike, setter, new Object[] { value });
285 }
286
287
288
289
290
291 private Map<String, Method> getAccessors() {
292 final Map<String, Method> propsAndAccessors = new HashMap<String, Method>();
293 final Method[] methods = beanLikeClass.getMethods();
294
295 for (final Method method : methods) {
296 final String methodName = method.getName();
297 if (methodName.startsWith("is")) {
298 propsAndAccessors.put(getPropertyNameFromMethodName(methodName), method);
299 }
300 else if (methodName.startsWith("get") && !methodName.equals("getClass") && (method.getParameterTypes().length == 0)) {
301 propsAndAccessors.put(getPropertyNameFromMethodName(methodName), method);
302 }
303 }
304 return propsAndAccessors;
305 }
306
307
308
309
310
311
312
313
314 private Object getNewInstance(Constructor<?> constructor, PropertiesAndValues propertiesAndValues) {
315 final Class<?>[] constructorSignature = constructor.getParameterTypes();
316
317 final List<String> propertyNames = constructorsSignaturesAndProperties.get(Arrays.asList(constructorSignature));
318 final List<Object> propertyValues = new ArrayList<Object>();
319
320 if (propertyNames != null) {
321 for (final String propertyName : propertyNames) {
322 propertyValues.add(propertiesAndValues.get(propertyName));
323 }
324 }
325 return createNewInstance(beanLikeClass.getName(), constructorSignature, propertyValues.toArray());
326 }
327
328
329
330
331
332 private Map<String, Method> getSetters() {
333 final Map<String, Method> propsAndSetters = new HashMap<String, Method>();
334 final Method[] methods = beanLikeClass.getMethods();
335
336 for (final Method method : methods) {
337 final String methodName = method.getName();
338 if (methodName.startsWith("set")) {
339 propsAndSetters.put(getPropertyNameFromMethodName(methodName), method);
340 }
341 }
342 return propsAndSetters;
343 }
344
345
346
347
348
349
350
351
352 private void verifyPropertyValuesFromAccessors(Object beanLike, List<String> propertiesToVerify, final PropertiesAndValues propertiesAndValuesExpected) {
353 for (final String propertyToVerify : propertiesToVerify) {
354 verifyPropertyValueFromAccessor(beanLike, propertyToVerify, propertiesAndValuesExpected.get(propertyToVerify));
355 }
356 }
357
358
359
360
361
362
363
364
365 private void verifyPropertyValueFromAccessor(Object beanLike, String propertyName, Object expectedValue) {
366 final Object returnedValue = getProperty(beanLike, propertyName);
367 if (areValuesDifferent(expectedValue, returnedValue)) {
368 throw new BeanLikeTesterException("The value of the property '" + propertyName + "' returned (" + returnedValue
369 + ") is not the same as the one expected (" + expectedValue + ")");
370 }
371 }
372
373 private Object getProperty(Object beanLike, String propertyName) {
374 final Method accessor = accessors.get(propertyName);
375 final Object returnedValue = invokeMethod(beanLike, accessor, (Object[]) null);
376 return returnedValue;
377 }
378
379
380
381
382
383
384 private void verifyPropertyNamesAreTheSameAs(Set<String> propertyNamesToTest) {
385 if (!gettablePropertyNames.equals(propertyNamesToTest)) {
386 throw new BeanLikeTesterException("The set of properties to test is different from the properties accessible from the object:\nFrom object: "
387 + gettablePropertyNames + "\nTo test: " + propertyNamesToTest);
388 }
389 }
390
391 private void verifyContainsAtLeastAllMutableProperties(Set<String> properties) {
392 if (!properties.containsAll(mutablePropertyNames)) {
393 throw new BeanLikeTesterException("The properties defined in parameter must at least contain all the settable properties of the object.\nParameter:"
394 + properties + "\nObject:" + mutablePropertyNames);
395 }
396 }
397
398 private Object createObjectWithSecificPropertySet(PropertiesAndValues propsWithDefaultValue, String property, Object value) {
399 final PropertiesAndValues propsWithValue = new PropertiesAndValues(propsWithDefaultValue);
400 propsWithValue.put(property, value);
401
402
403 if (settablePropertyNames.contains(property)) {
404 final Object object = createObjectWithDefaultValues(propsWithDefaultValue);
405 setProperty(object, property, value);
406 return object;
407 }
408
409
410 for (final Entry<List<Class<?>>, List<String>> entry : constructorsSignaturesAndProperties.entrySet()) {
411 final List<String> constructorPropertyNames = entry.getValue();
412 if (constructorPropertyNames.contains(property)) {
413 final Class<?>[] constructorSignature = entry.getKey().toArray(new Class[0]);
414 final List<Object> constructorValues = new ArrayList<Object>();
415 for (final String propertyName : constructorPropertyNames) {
416 constructorValues.add(propsWithValue.get(propertyName));
417 }
418 return createNewInstance(beanLikeClass.getName(), constructorSignature, constructorValues.toArray());
419 }
420 }
421 throw new RuntimeException("The property '" + property + "' must be settable by either a setter or a constructor!");
422 }
423
424 private Object createObjectWithDefaultValues(PropertiesAndValues propsWithDefaultValue) {
425
426 final Constructor<?> constructor = beanLikeClass.getConstructors()[0];
427 return getNewInstance(constructor, propsWithDefaultValue);
428 }
429
430 private void verifyAllValuesFromMutablePropsAreDifferent(PropertiesAndValues propsWithValue, PropertiesAndValues propsWithOtherValue) {
431 for (final Entry<String, Object> entry : propsWithValue.entrySet()) {
432 final String property = entry.getKey();
433 final Object value = entry.getValue();
434 if (mutablePropertyNames.contains(property) && !areValuesDifferent(value, propsWithOtherValue.get(property))) {
435 throw new BeanLikeTesterException("The value of the property '" + property + "' must be different in the parameters.");
436 }
437 }
438 }
439
440
441
442
443
444
445
446
447
448
449
450
451
452 public void testDefaultValues(PropertiesAndValues expectedDefaultValues) {
453
454 verifyPropertyNamesAreTheSameAs(expectedDefaultValues.keySet());
455
456 for (final Constructor<?> constructor : beanLikeClass.getConstructors()) {
457 final Object beanLike = getNewInstance(constructor, expectedDefaultValues);
458
459 for (final Entry<String, Method> entry : accessors.entrySet()) {
460 final String propertyName = entry.getKey();
461 final Object expected = expectedDefaultValues.get(propertyName);
462 final Object returned = getProperty(beanLike, propertyName);
463 if (areValuesDifferent(returned, expected)) {
464 throw new BeanLikeTesterException("The value of the property '" + propertyName + "' returned (" + returned
465 + ") is not the same as the one expected (" + expected + ")");
466 }
467 }
468 }
469 }
470
471
472
473
474
475
476
477
478
479
480
481 public void testMutatorsAndAccessors(PropertiesAndValues propsWithValue, PropertiesAndValues otherPropsWithValue) {
482 verifyContainsAtLeastAllMutableProperties(propsWithValue.keySet());
483 verifyContainsAtLeastAllMutableProperties(otherPropsWithValue.keySet());
484
485
486 for (final Constructor<?> constructor : beanLikeClass.getConstructors()) {
487 final List<Class<?>> constructorSignature = Arrays.asList(constructor.getParameterTypes());
488
489 final Object beanLike = getNewInstance(constructor, otherPropsWithValue);
490
491 if (!constructorSignature.isEmpty()) {
492 final List<String> propertyNamesInConstructor = constructorsSignaturesAndProperties.get(constructorSignature);
493 verifyPropertyValuesFromAccessors(beanLike, propertyNamesInConstructor, otherPropsWithValue);
494 }
495
496
497
498
499 final List<PropertiesAndValues> rounds = new ArrayList<PropertiesAndValues>();
500 rounds.add(propsWithValue);
501 rounds.add(otherPropsWithValue);
502
503 for (final PropertiesAndValues propertiesAndValues : rounds) {
504
505 for (final Entry<String, Method> entry : setters.entrySet()) {
506 final String propertyName = entry.getKey();
507 final Object valueToSet = propertiesAndValues.get(propertyName);
508 setProperty(beanLike, propertyName, valueToSet);
509 verifyPropertyValueFromAccessor(beanLike, propertyName, valueToSet);
510 }
511 }
512 }
513 }
514
515
516
517
518
519
520
521
522
523
524 public void testEqualsAndHash(PropertiesAndValues propsWithDefaultValue, PropertiesAndValues propsWithOtherValue) {
525 verifyContainsAtLeastAllMutableProperties(propsWithDefaultValue.keySet());
526 verifyContainsAtLeastAllMutableProperties(propsWithOtherValue.keySet());
527 verifyAllValuesFromMutablePropsAreDifferent(propsWithDefaultValue, propsWithOtherValue);
528
529 final Object defaultObj = createObjectWithDefaultValues(propsWithDefaultValue);
530 final int defaultHashCode = defaultObj.hashCode();
531
532
533 if (!defaultObj.equals(defaultObj)) {
534 throw new BeanLikeTesterException("The equals method must return true when the object is compared to itself:\nObject:" + defaultObj);
535 }
536
537 if (defaultObj.equals(null)) {
538 throw new BeanLikeTesterException("The comparison with null must return false.\nObject:" + defaultObj);
539 }
540
541 if (defaultObj.equals(AClassToBeTestedAgainst.instance)) {
542 throw new BeanLikeTesterException("The comparison with another class must return false.\nObject:" + defaultObj);
543 }
544
545 for (final Entry<String, Object> entry : propsWithOtherValue.entrySet()) {
546 final String property = entry.getKey();
547 final Object value = entry.getValue();
548
549 if (mutablePropertyNames.contains(property)) {
550 final Object otherObject1 = createObjectWithSecificPropertySet(propsWithDefaultValue, property, value);
551 final int otherHashCode1 = otherObject1.hashCode();
552
553 if (!areValuesDifferent(defaultHashCode, otherHashCode1)) {
554 throw new BeanLikeTesterException("The hashcodes of different objects should be different for the tests, please change the values or check that hashcode() is correct\nobject1:"
555 + defaultObj + " hashcode:" + defaultHashCode + "\nobject2:" + otherObject1 + " hashcode:" + otherHashCode1);
556 }
557
558
559 final boolean equals1 = otherObject1.equals(defaultObj);
560 final boolean equals2 = defaultObj.equals(otherObject1);
561 if (equals1 || equals2) {
562 throw new BeanLikeTesterException("The equals method must return false for the comparison between objects with different properties:\nobject1:"
563 + otherObject1 + "\nobject2:" + defaultObj);
564 }
565
566
567 final Object otherObject2 = createObjectWithSecificPropertySet(propsWithDefaultValue, property, value);
568 final int otherHashCode2 = otherObject2.hashCode();
569
570 if (!otherObject1.equals(otherObject2)) {
571 throw new BeanLikeTesterException("The equals method should return true for the comparison between objects with the same properties:\nobject1:"
572 + otherObject1 + "\nobject2:" + otherObject2);
573 }
574
575
576 if (!(otherHashCode1 == otherHashCode2)) {
577 throw new BeanLikeTesterException("The hashcodes must be equal:\nobject1:" + otherObject1 + " hashcode:" + otherHashCode1 + "\nobject2:"
578 + otherObject2 + " hashcode:" + otherHashCode2);
579 }
580
581 }
582 }
583 }
584
585
586
587
588
589
590
591
592
593
594 public void testToString(PropertiesAndValues propsWithDefaultValue, PropertiesAndValues propsWithOtherValue) {
595 verifyContainsAtLeastAllMutableProperties(propsWithDefaultValue.keySet());
596 verifyContainsAtLeastAllMutableProperties(propsWithOtherValue.keySet());
597 verifyAllValuesFromMutablePropsAreDifferent(propsWithDefaultValue, propsWithOtherValue);
598
599 final Object defaultObj = createObjectWithDefaultValues(propsWithDefaultValue);
600 final String toStringFromDefaultValues = defaultObj.toString();
601
602 for (final Entry<String, Object> entry : propsWithOtherValue.entrySet()) {
603 final String property = entry.getKey();
604 final Object value = entry.getValue();
605
606 if (mutablePropertyNames.contains(property)) {
607 final Object object = createObjectWithSecificPropertySet(propsWithDefaultValue, property, value);
608 final String toStringFromOtherValues = object.toString();
609 if (!areValuesDifferent(toStringFromDefaultValues, toStringFromOtherValues)) {
610 throw new BeanLikeTesterException("The result of toString() should depend on the property '" + property + "'");
611 }
612 }
613 }
614 }
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631 public void testBeanLike(PropertiesAndValues propsWithDefaultValue, PropertiesAndValues propsWithOtherValue) {
632 testDefaultValues(propsWithDefaultValue);
633 testMutatorsAndAccessors(propsWithDefaultValue, propsWithOtherValue);
634 testEqualsAndHash(propsWithDefaultValue, propsWithOtherValue);
635 testToString(propsWithDefaultValue, propsWithOtherValue);
636 }
637
638 }