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