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.lang.reflect.Modifier;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.Date;
44 import java.util.HashSet;
45 import java.util.List;
46 import java.util.Set;
47
48 import lombok.Data;
49
50 import nl.jqno.equalsverifier.EqualsVerifier;
51 import nl.jqno.equalsverifier.Warning;
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 Externalizable || object instanceof Serializable;
361 }
362
363
364
365
366
367
368
369
370
371 @SuppressWarnings("unchecked")
372 T canSerialize(final T object) {
373 byte[] byteArray = null;
374
375
376 try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
377 ObjectOutputStream oos = new ObjectOutputStream(baos)) {
378 oos.writeObject(object);
379 byteArray = baos.toByteArray();
380 } catch (final IOException e) {
381 Assertions.fail(String.format("An exception was thrown while serializing the class '%s': '%s',",
382 object.getClass().getName(), e.toString()));
383 return null;
384 }
385
386
387 try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
388 ObjectInputStream ois = new ObjectInputStream(bais)) {
389 return (T) ois.readObject();
390 } catch (final ClassNotFoundException | IOException e) {
391 Assertions.fail(String.format("An exception was thrown while deserializing the class '%s': '%s',",
392 object.getClass().getName(), e.toString()));
393 }
394 return null;
395 }
396
397
398
399
400
401
402
403
404
405
406
407
408
409 private <R> Object buildValue(final Class<R> returnType, final LoadType loadType) {
410 final ValueBuilder valueBuilder = new ValueBuilder();
411 valueBuilder.setLoadData(this.loadData);
412 return valueBuilder.buildValue(returnType, loadType);
413 }
414
415
416
417
418 public void equalsHashCodeToStringSymmetricTest() {
419
420 try {
421 EqualsVerifier.simple().forClass(this.clazz).suppress(Warning.BIGDECIMAL_EQUALITY).verify();
422 } catch (AssertionError e) {
423 JavaBeanTesterWorker.logger.warn("EqualsVerifier attempt failed: {}", e.getMessage());
424 }
425
426
427 final T x = new ClassInstance<T>().newInstance(this.clazz);
428 final T y = new ClassInstance<T>().newInstance(this.clazz);
429
430 Assertions.assertNotNull(x,
431 String.format("Create new instance of class '%s' resulted in null", this.clazz.getName()));
432 Assertions.assertNotNull(y,
433 String.format("Create new instance of class '%s' resulted in null", this.clazz.getName()));
434
435
436
437
438
439
440 final E ext = new ClassInstance<E>().newInstance(this.extension);
441
442 Assertions.assertNotNull(ext,
443 String.format("Create new instance of extension %s resulted in null", this.extension.getName()));
444
445
446 Assertions.assertEquals(x, y,
447 String.format(".equals() should be consistent for two empty objects of type %s", this.clazz.getName()));
448 Assertions.assertEquals(x.hashCode(), y.hashCode(), String
449 .format(".hashCode() should be consistent for two empty objects of type %s", this.clazz.getName()));
450 Assertions.assertEquals(x.toString(), y.toString(), String
451 .format(".toString() should be consistent for two empty objects of type %s", this.clazz.getName()));
452
453
454 Assertions.assertNotEquals(ext, y,
455 String.format(".equals() 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.hashCode(), y.hashCode(),
458 String.format(".hashCode() should not be equal for extension of type %s and empty object of type %s",
459 this.extension.getName(), this.clazz.getName()));
460 Assertions.assertNotEquals(ext.toString(), y.toString(),
461 String.format(".toString() should not be equal for extension of type %s and empty object of type %s",
462 this.extension.getName(), this.clazz.getName()));
463
464
465 Assertions.assertNotEquals(x, null,
466 String.format("An empty object of type %s should not be equal to null", this.clazz.getName()));
467 Assertions.assertEquals(x, x,
468 String.format("An empty object of type %s should be equal to itself", this.clazz.getName()));
469
470
471 Assertions.assertNotEquals(ext, null,
472 String.format("An empty extension of type %s should not be equal to null", this.clazz.getName()));
473 Assertions.assertEquals(ext, ext,
474 String.format("An empty extension of type %s should be equal to itself", this.extension.getName()));
475
476
477
478 if (this.classHasSetters(this.clazz)) {
479
480 JavaBeanTesterWorker.load(this.clazz, x, this.loadData);
481
482
483 JavaBeanTesterWorker.load(this.extension, ext, this.loadData);
484
485
486 Assertions.assertNotEquals(y, x,
487 String.format(".equals() should not be consistent for one empty and one loaded object of type %s",
488 this.clazz.getName()));
489
490
491 Assertions.assertNotEquals(y, ext,
492 String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
493 this.extension.getName(), this.clazz.getName()));
494
495
496 JavaBeanTesterWorker.load(this.clazz, y, this.loadData);
497
498
499 if (this.loadData == LoadData.ON) {
500 Assertions.assertEquals(x, y,
501 String.format(".equals() should be equal for two instances of type %s with loaded data",
502 this.clazz.getName()));
503 Assertions.assertEquals(x.hashCode(), y.hashCode(),
504 String.format(".hashCode() should be equal for two instances of type %s with loaded data",
505 this.clazz.getName()));
506 } else {
507 Assertions.assertNotEquals(x, y);
508 Assertions.assertNotEquals(x.hashCode(), y.hashCode());
509 }
510
511
512 Assertions.assertNotEquals(ext, y,
513 String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
514 this.extension.getName(), this.clazz.getName()));
515 Assertions.assertNotEquals(ext.hashCode(), y.hashCode(),
516 String.format(
517 ".hashCode() should not be equal for extension of type %s and empty object of type %s",
518 this.extension.getName(), this.clazz.getName()));
519 Assertions.assertNotEquals(ext.toString(), y.toString(),
520 String.format(
521 ".toString() should not be equal for extension of type %s and empty object of type %s",
522 this.extension.getName(), this.clazz.getName()));
523 }
524
525
526 try {
527 final T e = new ClassInstance<T>().newInstance(this.clazz);
528 ByteBuddyBeanCopier.copy(x, e, (value, targetType) -> {
529 if (targetType == boolean.class) {
530 return value == null ? Boolean.FALSE : value;
531 }
532 return value;
533 });
534 Assertions.assertEquals(e, x);
535 } catch (final Exception e) {
536 JavaBeanTesterWorker.logger.trace("Do nothing class is not mutable", e);
537 }
538
539
540 if (Modifier.isFinal(clazz.getModifiers())) {
541 JavaBeanTesterWorker.logger.trace("Final object does not go through final equals check");
542 return;
543 }
544
545
546 try {
547 final E e = new ClassInstance<E>().newInstance(this.extension);
548 ByteBuddyBeanCopier.copy(ext, e, (value, targetType) -> {
549 if (targetType == boolean.class) {
550 return value == null ? Boolean.FALSE : value;
551 }
552 return value;
553 });
554 Assertions.assertEquals(e, ext);
555 } catch (final Exception e) {
556 JavaBeanTesterWorker.logger.trace("Do nothing class is not mutable", e);
557 }
558 }
559
560
561
562
563
564
565
566
567
568
569 void equalsTests(final T instance, final T expected) {
570
571
572
573 if (expected.hashCode() == instance.hashCode()) {
574 Assertions.assertEquals(expected.hashCode(), instance.hashCode());
575 } else {
576 Assertions.assertNotEquals(expected.hashCode(), instance.hashCode());
577 }
578
579 final ValueBuilder valueBuilder = new ValueBuilder();
580 valueBuilder.setLoadData(this.loadData);
581
582 final PropertyDescriptor[] props = this.getProps(instance.getClass());
583 for (final PropertyDescriptor prop : props) {
584 Method getter = prop.getReadMethod();
585 final Method setter = prop.getWriteMethod();
586
587
588 if (getter == null && setter != null) {
589 final String isBooleanWrapper = "is" + setter.getName().substring(3);
590 try {
591 getter = this.clazz.getMethod(isBooleanWrapper);
592 } catch (NoSuchMethodException | SecurityException e) {
593
594 }
595 }
596
597 if (getter != null && setter != null) {
598
599 final Class<?> returnType = getter.getReturnType();
600 final Class<?>[] params = setter.getParameterTypes();
601
602 if (params.length == 1 && params[0] == returnType) {
603
604
605 try {
606
607 final Object original = getter.invoke(instance);
608
609
610 Object value = valueBuilder.buildValue(returnType, LoadType.ALTERNATE_DATA);
611
612
613 setter.invoke(instance, value);
614
615
616 if (instance.equals(expected)) {
617 Assertions.assertEquals(expected, instance);
618 } else {
619 Assertions.assertNotEquals(expected, instance);
620 }
621
622
623 value = valueBuilder.buildValue(returnType, LoadType.NULL_DATA);
624
625
626 setter.invoke(instance, value);
627
628
629 if (instance.equals(expected)) {
630 Assertions.assertEquals(expected, instance);
631 } else {
632 Assertions.assertNotEquals(expected, instance);
633 }
634
635
636 setter.invoke(instance, original);
637
638 } catch (final IllegalAccessException | IllegalArgumentException | InvocationTargetException
639 | SecurityException e) {
640 Assertions.fail(
641 String.format("An exception was thrown while testing the property (equals) '%s': '%s'",
642 prop.getName(), e.toString()));
643 }
644 }
645 }
646 }
647 }
648
649
650
651
652
653
654
655
656
657 private boolean classHasSetters(final Class<T> clazz) {
658 return Arrays.stream(this.getProps(clazz))
659 .anyMatch(propertyDescriptor -> propertyDescriptor.getWriteMethod() != null);
660 }
661
662
663
664
665
666
667
668
669
670 private PropertyDescriptor[] getProps(final Class<?> clazz) {
671 try {
672 final List<PropertyDescriptor> usedProps = new ArrayList<>(
673 Introspector.getBeanInfo(clazz).getPropertyDescriptors().length);
674 final List<PropertyDescriptor> props = Arrays
675 .asList(Introspector.getBeanInfo(clazz).getPropertyDescriptors());
676 for (final PropertyDescriptor prop : props) {
677
678 if (this.skipThese.contains(prop.getName())) {
679 continue;
680 }
681 usedProps.add(prop);
682 }
683 return usedProps.toArray(new PropertyDescriptor[0]);
684 } catch (final IntrospectionException e) {
685 Assertions.fail(String.format("An exception was thrown while testing class '%s': '%s'",
686 this.clazz.getName(), e.toString()));
687 return new PropertyDescriptor[0];
688 }
689 }
690
691 }