1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package com.codebox.bean;
16
17 import com.codebox.enums.CheckClear;
18 import com.codebox.enums.CheckConstructor;
19 import com.codebox.enums.CheckEquals;
20 import com.codebox.enums.CheckSerialize;
21 import com.codebox.enums.LoadData;
22 import com.codebox.enums.LoadType;
23 import com.codebox.enums.SkipStrictSerialize;
24 import com.codebox.instance.ClassInstance;
25
26 import java.beans.IntrospectionException;
27 import java.beans.Introspector;
28 import java.beans.PropertyDescriptor;
29 import java.io.ByteArrayInputStream;
30 import java.io.ByteArrayOutputStream;
31 import java.io.Externalizable;
32 import java.io.IOException;
33 import java.io.ObjectInputStream;
34 import java.io.ObjectOutputStream;
35 import java.io.Serializable;
36 import java.lang.annotation.Annotation;
37 import java.lang.reflect.Constructor;
38 import java.lang.reflect.InvocationTargetException;
39 import java.lang.reflect.Method;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.Date;
43 import java.util.HashSet;
44 import java.util.List;
45 import java.util.Set;
46
47 import lombok.Data;
48
49 import net.sf.cglib.beans.BeanCopier;
50
51 import nl.jqno.equalsverifier.EqualsVerifier;
52
53 import org.junit.jupiter.api.Assertions;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56
57
58
59
60
61
62
63
64
65 @Data
66 class JavaBeanTesterWorker<T, E> {
67
68
69 private static final Logger LOGGER = LoggerFactory.getLogger(JavaBeanTesterWorker.class);
70
71
72 private CheckClear checkClear;
73
74
75 private CheckConstructor checkConstructor;
76
77
78 private CheckEquals checkEquals;
79
80
81 private CheckSerialize checkSerializable;
82
83
84 private LoadData loadData;
85
86
87 private final Class<T> clazz;
88
89
90 private Class<E> extension;
91
92
93 private SkipStrictSerialize skipStrictSerializable;
94
95
96 private Set<String> skipThese = new HashSet<>();
97
98
99
100
101
102
103
104 JavaBeanTesterWorker(final Class<T> newClazz) {
105 this.clazz = newClazz;
106 }
107
108
109
110
111
112
113
114
115
116 JavaBeanTesterWorker(final Class<T> newClazz, final Class<E> newExtension) {
117 this.clazz = newClazz;
118 this.extension = newExtension;
119 }
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137 public static <L> JavaBeanTesterWorker<L, Object> load(final Class<L> clazz, final L instance,
138 final LoadData loadData, final String... skipThese) {
139 final JavaBeanTesterWorker<L, Object> worker = new JavaBeanTesterWorker<>(clazz);
140
141 worker.setLoadData(loadData);
142 if (skipThese != null) {
143 worker.setSkipThese(new HashSet<>(Arrays.asList(skipThese)));
144 }
145 worker.getterSetterTests(instance);
146
147 return worker;
148 }
149
150
151
152
153
154 public void test() {
155
156
157 this.getterSetterTests(new ClassInstance<T>().newInstance(this.clazz));
158
159
160 if (this.checkClear != CheckClear.OFF) {
161 this.clearTest();
162 }
163
164
165 if (this.checkConstructor != CheckConstructor.OFF) {
166 this.constructorsTest();
167 }
168
169
170 this.checkSerializableTest();
171
172
173 if (this.checkEquals == CheckEquals.ON) {
174 this.equalsHashCodeToStringSymmetricTest();
175 }
176
177 }
178
179
180
181
182
183
184
185
186
187 void getterSetterTests(final T instance) {
188 final PropertyDescriptor[] props = this.getProps(this.clazz);
189 for (final PropertyDescriptor prop : props) {
190 Method getter = prop.getReadMethod();
191 final Method setter = prop.getWriteMethod();
192
193
194 if (getter == null && setter != null) {
195 final String isBooleanWrapper = "is" + setter.getName().substring(3);
196 try {
197 getter = this.clazz.getMethod(isBooleanWrapper);
198 } catch (NoSuchMethodException | SecurityException e) {
199
200 }
201 }
202
203 if (getter != null && setter != null) {
204
205 final Class<?> returnType = getter.getReturnType();
206 final Class<?>[] params = setter.getParameterTypes();
207
208 if (params.length == 1 && params[0] == returnType) {
209
210
211 try {
212
213 final Object value = this.buildValue(returnType, LoadType.STANDARD_DATA);
214
215
216 final T bean = new ClassInstance<T>().newInstance(this.clazz);
217
218
219 setter.invoke(bean, value);
220
221
222 setter.invoke(instance, value);
223
224 final Object expectedValue = value;
225 Object actualValue = getter.invoke(bean);
226
227
228
229
230
231
232
233
234
235
236
237
238 if (this.clazz == Date.class && prop.getName().equals("month")
239 && expectedValue.equals(Integer.valueOf("1"))
240 && actualValue.equals(Integer.valueOf("2"))) {
241 actualValue = Integer.valueOf("1");
242 }
243
244 Assertions.assertEquals(expectedValue, actualValue,
245 String.format("Failed while testing property '%s' of class '%s'", prop.getName(),
246 this.clazz.getName()));
247
248 } catch (final IllegalAccessException | IllegalArgumentException | InvocationTargetException
249 | SecurityException e) {
250 Assertions.fail(String.format(
251 "An exception was thrown while testing class '%s' with the property (getter/setter) '%s': '%s'",
252 this.clazz.getName(), prop.getName(), e.toString()));
253 }
254 }
255 }
256 }
257 }
258
259
260
261
262 void clearTest() {
263 final Method[] methods = this.clazz.getDeclaredMethods();
264 for (final Method method : methods) {
265 if (method.getName().equals("clear")) {
266 final T newClass = new ClassInstance<T>().newInstance(this.clazz);
267 final T expectedClass = new ClassInstance<T>().newInstance(this.clazz);
268 try {
269
270 List<Annotation> annotations = null;
271 for (final Method mt : methods) {
272 annotations = Arrays.asList(mt.getAnnotations());
273 for (final Annotation annotation : annotations) {
274
275 if ("PostConstruct".equals(annotation.annotationType().getSimpleName())
276 && mt.getParameterTypes().length == 0) {
277
278 mt.invoke(newClass);
279
280 mt.invoke(expectedClass);
281 }
282 }
283 }
284
285 newClass.getClass().getMethod("clear").invoke(newClass);
286 Assertions.assertEquals(expectedClass, newClass,
287 String.format("Clear method does not match new object '%s'", this.clazz));
288 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
289 | NoSuchMethodException | SecurityException e) {
290 Assertions.fail(String.format("An exception was thrown while testing the Clear method '%s' : '%s'",
291 this.clazz.getName(), e.toString()));
292 }
293 }
294 }
295 }
296
297
298
299
300 void constructorsTest() {
301 for (final Constructor<?> constructor : this.clazz.getConstructors()) {
302
303
304 if (constructor.isAnnotationPresent(Deprecated.class)) {
305 continue;
306 }
307
308 final Class<?>[] types = constructor.getParameterTypes();
309
310 final Object[] values = new Object[constructor.getParameterTypes().length];
311
312
313 for (int i = 0; i < values.length; i++) {
314 values[i] = this.buildValue(types[i], LoadType.STANDARD_DATA);
315 }
316
317 try {
318 constructor.newInstance(values);
319 } catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) {
320 Assertions.fail(
321 String.format("An exception was thrown while testing the constructor(s) '%s' with '%s': '%s'",
322 constructor.getName(), Arrays.toString(values), e.toString()));
323 }
324
325
326 }
327 }
328
329
330
331
332 void checkSerializableTest() {
333 final T object = new ClassInstance<T>().newInstance(this.clazz);
334 if (this.implementsSerializable(object)) {
335 final T newObject = this.canSerialize(object);
336
337 if (this.skipStrictSerializable != SkipStrictSerialize.ON) {
338 Assertions.assertEquals(object, newObject);
339 } else {
340 Assertions.assertNotEquals(object, newObject);
341 }
342 return;
343 }
344
345
346 if (this.checkSerializable == CheckSerialize.ON) {
347 Assertions.fail(String.format("Class is not serializable '%s'", object.getClass().getName()));
348 }
349 }
350
351
352
353
354
355
356
357
358
359 boolean implementsSerializable(final T object) {
360 return object instanceof Serializable || object instanceof Externalizable;
361 }
362
363
364
365
366
367
368
369
370
371 @SuppressWarnings("unchecked")
372 T canSerialize(final T object) {
373
374 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
375 try {
376 new ObjectOutputStream(baos).writeObject(object);
377 } catch (final IOException e) {
378 Assertions.fail(String.format("An exception was thrown while serializing the class '%s': '%s',",
379 object.getClass().getName(), e.toString()));
380 return null;
381 }
382
383
384 final ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
385 try {
386 return (T) new ObjectInputStream(bais).readObject();
387 } catch (final ClassNotFoundException | IOException e) {
388 Assertions.fail(String.format("An exception was thrown while deserializing the class '%s': '%s',",
389 object.getClass().getName(), e.toString()));
390 }
391 return null;
392 }
393
394
395
396
397
398
399
400
401
402
403
404
405
406 private <R> Object buildValue(final Class<R> returnType, final LoadType loadType) {
407 final ValueBuilder valueBuilder = new ValueBuilder();
408 valueBuilder.setLoadData(this.loadData);
409 return valueBuilder.buildValue(returnType, loadType);
410 }
411
412
413
414
415 public void equalsHashCodeToStringSymmetricTest() {
416
417 try {
418 EqualsVerifier.simple().forClass(this.clazz).verify();
419 } catch (AssertionError e) {
420 JavaBeanTesterWorker.LOGGER.warn("EqualsVerifier attempt failed: {}", e.getMessage());
421 }
422
423
424 final T x = new ClassInstance<T>().newInstance(this.clazz);
425 final T y = new ClassInstance<T>().newInstance(this.clazz);
426
427 Assertions.assertNotNull(x,
428 String.format("Create new instance of class '%s' resulted in null", this.clazz.getName()));
429 Assertions.assertNotNull(y,
430 String.format("Create new instance of class '%s' resulted in null", this.clazz.getName()));
431
432
433
434
435
436
437 final E ext = new ClassInstance<E>().newInstance(this.extension);
438
439 Assertions.assertNotNull(ext,
440 String.format("Create new instance of extension %s resulted in null", this.extension.getName()));
441
442
443 Assertions.assertEquals(x, y,
444 String.format(".equals() should be consistent for two empty objects of type %s", this.clazz.getName()));
445 Assertions.assertEquals(x.hashCode(), y.hashCode(), String
446 .format(".hashCode() should be consistent for two empty objects of type %s", this.clazz.getName()));
447 Assertions.assertEquals(x.toString(), y.toString(), String
448 .format(".toString() should be consistent for two empty objects of type %s", this.clazz.getName()));
449
450
451 Assertions.assertNotEquals(ext, y,
452 String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
453 this.extension.getName(), this.clazz.getName()));
454 Assertions.assertNotEquals(ext.hashCode(), y.hashCode(),
455 String.format(".hashCode() should not be equal for extension of type %s and empty object of type %s",
456 this.extension.getName(), this.clazz.getName()));
457 Assertions.assertNotEquals(ext.toString(), y.toString(),
458 String.format(".toString() should not be equal for extension of type %s and empty object of type %s",
459 this.extension.getName(), this.clazz.getName()));
460
461
462 Assertions.assertNotEquals(x, null,
463 String.format("An empty object of type %s should not be equal to null", this.clazz.getName()));
464 Assertions.assertEquals(x, x,
465 String.format("An empty object of type %s should be equal to itself", this.clazz.getName()));
466
467
468 Assertions.assertNotEquals(ext, null,
469 String.format("An empty extension of type %s should not be equal to null", this.clazz.getName()));
470 Assertions.assertEquals(ext, ext,
471 String.format("An empty extension of type %s should be equal to itself", this.extension.getName()));
472
473
474
475 if (this.classHasSetters(this.clazz)) {
476
477 JavaBeanTesterWorker.load(this.clazz, x, this.loadData);
478
479
480 JavaBeanTesterWorker.load(this.extension, ext, this.loadData);
481
482
483 Assertions.assertNotEquals(y, x,
484 String.format(".equals() should not be consistent for one empty and one loaded object of type %s",
485 this.clazz.getName()));
486
487
488 Assertions.assertNotEquals(y, ext,
489 String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
490 this.extension.getName(), this.clazz.getName()));
491
492
493 JavaBeanTesterWorker.load(this.clazz, y, this.loadData);
494
495
496 if (this.loadData == LoadData.ON) {
497 Assertions.assertEquals(x, y,
498 String.format(".equals() should be equal for two instances of type %s with loaded data",
499 this.clazz.getName()));
500 Assertions.assertEquals(x.hashCode(), y.hashCode(),
501 String.format(".hashCode() should be equal for two instances of type %s with loaded data",
502 this.clazz.getName()));
503 } else {
504 Assertions.assertNotEquals(x, y);
505 Assertions.assertNotEquals(x.hashCode(), y.hashCode());
506 }
507
508
509 Assertions.assertNotEquals(ext, y,
510 String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
511 this.extension.getName(), this.clazz.getName()));
512 Assertions.assertNotEquals(ext.hashCode(), y.hashCode(),
513 String.format(
514 ".hashCode() should not be equal for extension of type %s and empty object of type %s",
515 this.extension.getName(), this.clazz.getName()));
516 Assertions.assertNotEquals(ext.toString(), y.toString(),
517 String.format(
518 ".toString() should not be equal for extension of type %s and empty object of type %s",
519 this.extension.getName(), this.clazz.getName()));
520 }
521
522
523 try {
524 final BeanCopier clazzBeanCopier = BeanCopier.create(this.clazz, this.clazz, true);
525 final T e = new ClassInstance<T>().newInstance(this.clazz);
526 clazzBeanCopier.copy(x, e, null);
527 Assertions.assertEquals(e, x);
528 } catch (final Exception e) {
529 JavaBeanTesterWorker.LOGGER.trace("Do nothing class is not mutable", e);
530 }
531
532
533 try {
534 final BeanCopier extensionBeanCopier = BeanCopier.create(this.extension, this.extension, true);
535 final E e = new ClassInstance<E>().newInstance(this.extension);
536 extensionBeanCopier.copy(ext, e, null);
537 Assertions.assertEquals(e, ext);
538 } catch (final Exception e) {
539 JavaBeanTesterWorker.LOGGER.trace("Do nothing class is not mutable", e);
540 }
541 }
542
543
544
545
546
547
548
549
550
551
552 void equalsTests(final T instance, final T expected) {
553
554
555
556 if (expected.hashCode() == instance.hashCode()) {
557 Assertions.assertEquals(expected.hashCode(), instance.hashCode());
558 } else {
559 Assertions.assertNotEquals(expected.hashCode(), instance.hashCode());
560 }
561
562 final ValueBuilder valueBuilder = new ValueBuilder();
563 valueBuilder.setLoadData(this.loadData);
564
565 final PropertyDescriptor[] props = this.getProps(instance.getClass());
566 for (final PropertyDescriptor prop : props) {
567 Method getter = prop.getReadMethod();
568 final Method setter = prop.getWriteMethod();
569
570
571 if (getter == null && setter != null) {
572 final String isBooleanWrapper = "is" + setter.getName().substring(3);
573 try {
574 getter = this.clazz.getMethod(isBooleanWrapper);
575 } catch (NoSuchMethodException | SecurityException e) {
576
577 }
578 }
579
580 if (getter != null && setter != null) {
581
582 final Class<?> returnType = getter.getReturnType();
583 final Class<?>[] params = setter.getParameterTypes();
584
585 if (params.length == 1 && params[0] == returnType) {
586
587
588 try {
589
590 final Object original = getter.invoke(instance);
591
592
593 Object value = valueBuilder.buildValue(returnType, LoadType.ALTERNATE_DATA);
594
595
596 setter.invoke(instance, value);
597
598
599 if (instance.equals(expected)) {
600 Assertions.assertEquals(expected, instance);
601 } else {
602 Assertions.assertNotEquals(expected, instance);
603 }
604
605
606 value = valueBuilder.buildValue(returnType, LoadType.NULL_DATA);
607
608
609 setter.invoke(instance, value);
610
611
612 if (instance.equals(expected)) {
613 Assertions.assertEquals(expected, instance);
614 } else {
615 Assertions.assertNotEquals(expected, instance);
616 }
617
618
619 setter.invoke(instance, original);
620
621 } catch (final IllegalAccessException | IllegalArgumentException | InvocationTargetException
622 | SecurityException e) {
623 Assertions.fail(
624 String.format("An exception was thrown while testing the property (equals) '%s': '%s'",
625 prop.getName(), e.toString()));
626 }
627 }
628 }
629 }
630 }
631
632
633
634
635
636
637
638
639
640 private boolean classHasSetters(final Class<T> clazz) {
641 return Arrays.stream(this.getProps(clazz))
642 .anyMatch(propertyDescriptor -> propertyDescriptor.getWriteMethod() != null);
643 }
644
645
646
647
648
649
650
651
652
653 private PropertyDescriptor[] getProps(final Class<?> clazz) {
654 try {
655 final List<PropertyDescriptor> usedProps = new ArrayList<>(
656 Introspector.getBeanInfo(clazz).getPropertyDescriptors().length);
657 final List<PropertyDescriptor> props = Arrays
658 .asList(Introspector.getBeanInfo(clazz).getPropertyDescriptors());
659 nextProp: for (final PropertyDescriptor prop : props) {
660
661 for (final String skipThis : this.skipThese) {
662 if (skipThis.equals(prop.getName())) {
663 continue nextProp;
664 }
665 }
666 usedProps.add(prop);
667 }
668 return usedProps.toArray(new PropertyDescriptor[usedProps.size()]);
669 } catch (final IntrospectionException e) {
670 Assertions.fail(String.format("An exception was thrown while testing class '%s': '%s'",
671 this.clazz.getName(), e.toString()));
672 return new PropertyDescriptor[0];
673 }
674 }
675
676 }