View Javadoc
1   /*
2    * SPDX-License-Identifier: Apache-2.0
3    * See LICENSE file for details.
4    *
5    * Copyright 2012-2026 hazendaz
6    *
7    * Portions of initial baseline code (getter/setter test) by Rob Dawson (CodeBox)
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   * The Class JavaBeanTesterWorker.
53   *
54   * @param <T>
55   *            the generic type
56   * @param <E>
57   *            the element type
58   */
59  @Data
60  class JavaBeanTesterWorker<T, E> {
61  
62      /** The Constant logger. */
63      private static final Logger logger = LoggerFactory.getLogger(JavaBeanTesterWorker.class);
64  
65      /** The check clear. */
66      private CheckClear checkClear;
67  
68      /** The check constructor. */
69      private CheckConstructor checkConstructor;
70  
71      /** The check equals. */
72      private CheckEquals checkEquals;
73  
74      /** The check serializable. */
75      private CheckSerialize checkSerializable;
76  
77      /** The load data. */
78      private LoadData loadData;
79  
80      /** The clazz. */
81      private final Class<T> clazz;
82  
83      /** The extension. */
84      private Class<E> extension;
85  
86      /** The skip strict serialize. */
87      private SkipStrictSerialize skipStrictSerializable;
88  
89      /** The skip these. */
90      private Set<String> skipThese = new HashSet<>();
91  
92      /**
93       * Instantiates a new java bean tester worker.
94       *
95       * @param newClazz
96       *            the clazz
97       */
98      JavaBeanTesterWorker(final Class<T> newClazz) {
99          this.clazz = newClazz;
100     }
101 
102     /**
103      * Instantiates a new java bean tester worker.
104      *
105      * @param newClazz
106      *            the clazz
107      * @param newExtension
108      *            the extension
109      */
110     JavaBeanTesterWorker(final Class<T> newClazz, final Class<E> newExtension) {
111         this.clazz = newClazz;
112         this.extension = newExtension;
113     }
114 
115     /**
116      * Tests the load methods of the specified class.
117      *
118      * @param <L>
119      *            the type parameter associated with the class under test.
120      * @param clazz
121      *            the class under test.
122      * @param instance
123      *            the instance of class under test.
124      * @param loadData
125      *            load recursively all underlying data objects.
126      * @param skipThese
127      *            the names of any properties that should not be tested.
128      *
129      * @return the java bean tester worker
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      * Tests the clear, get, set, equals, hashCode, toString, serializable, and constructor(s) methods of the specified
146      * class.
147      */
148     public void test() {
149 
150         // Test Getter/Setter
151         this.getterSetterTests(new ClassInstance<T>().newInstance(this.clazz));
152 
153         // Test Clear
154         if (this.checkClear != CheckClear.OFF) {
155             this.clearTest();
156         }
157 
158         // Test constructor
159         if (this.checkConstructor != CheckConstructor.OFF) {
160             this.constructorsTest();
161         }
162 
163         // Test Serializable (internally uses on/off/strict checks)
164         this.checkSerializableTest();
165 
166         // Test Equals
167         if (this.checkEquals == CheckEquals.ON) {
168             this.equalsHashCodeToStringSymmetricTest();
169         }
170 
171     }
172 
173     /**
174      * Getter Setter Tests.
175      *
176      * @param instance
177      *            the instance of class under test.
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             // Java Metro Bug Patch (Boolean Wrapper usage of 'is' possible
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                     // Do nothing
192                 }
193             }
194 
195             if (getter != null && setter != null) {
196                 // We have both a get and set method for this property
197                 final Class<?> returnType = getter.getReturnType();
198                 final Class<?>[] params = setter.getParameterTypes();
199 
200                 if (params.length == 1 && params[0] == returnType) {
201                     // The set method has 1 argument, which is of the same type as the return type of the get method, so
202                     // we can test this property
203                     try {
204                         // Build a value of the correct type to be passed to the set method
205                         final Object value = this.buildValue(returnType, LoadType.STANDARD_DATA);
206 
207                         // Build an instance of the bean that we are testing (each property test gets a new instance)
208                         final T bean = new ClassInstance<T>().newInstance(this.clazz);
209 
210                         // Call the set method, then check the same value comes back out of the get method
211                         setter.invoke(bean, value);
212 
213                         // Use data set on instance
214                         setter.invoke(instance, value);
215 
216                         final Object expectedValue = value;
217                         Object actualValue = getter.invoke(bean);
218 
219                         // java.util.Date normalization patch
220                         //
221                         // Date is zero based so it adds 1 through normalization. Since we always pass '1' here, it is
222                         // the same as stating February. Thus we roll over the month quite often into March towards
223                         // end of the month resulting in '1' != '2' situation. The reason we pass '1' is that we are
224                         // testing the content of the object and have no idea it is a date to start with. It is simply
225                         // that it sees getters/setters and tries to load them appropriately. The underlying problem
226                         // with that is that the Date object performs normalization to avoid dates like 2-30 that do
227                         // not exist and is not a typical getter/setter use-case. It is also deprecated but we don't
228                         // want to simply skip all deprecated items as we intend to test as much as possible.
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      * Clear test.
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                     // Perform any Post Construction on object without parameters
262                     List<Annotation> annotations = null;
263                     for (final Method mt : methods) {
264                         annotations = Arrays.asList(mt.getAnnotations());
265                         for (final Annotation annotation : annotations) {
266                             // XXX On purpose logic change to support both javax and jakarta namespace for annotations
267                             if ("PostConstruct".equals(annotation.annotationType().getSimpleName())
268                                     && mt.getParameterTypes().length == 0) {
269                                 // Invoke method newClass
270                                 mt.invoke(newClass);
271                                 // Invoke method expectedClass
272                                 mt.invoke(expectedClass);
273                             }
274                         }
275                     }
276                     // Invoke clear only on newClass
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      * Constructors test.
291      */
292     void constructorsTest() {
293         for (final Constructor<?> constructor : this.clazz.getConstructors()) {
294 
295             // Skip deprecated constructors
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             // Load Data
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             // TODO 1/12/2019 JWL Add checking of new object properties
318         }
319     }
320 
321     /**
322      * Check Serializable test.
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             // Toggle to throw or not throw error with only one way working
329             if (this.skipStrictSerializable != SkipStrictSerialize.ON) {
330                 Assertions.assertEquals(object, newObject);
331             } else {
332                 Assertions.assertNotEquals(object, newObject);
333             }
334             return;
335         }
336 
337         // Only throw error when specifically checking on serialization
338         if (this.checkSerializable == CheckSerialize.ON) {
339             Assertions.fail(String.format("Class is not serializable '%s'", object.getClass().getName()));
340         }
341     }
342 
343     /**
344      * Implements serializable.
345      *
346      * @param object
347      *            the object
348      *
349      * @return true, if successful
350      */
351     boolean implementsSerializable(final T object) {
352         return object instanceof Externalizable || object instanceof Serializable;
353     }
354 
355     /**
356      * Can serialize.
357      *
358      * @param object
359      *            the object
360      *
361      * @return object read after serialization
362      */
363     @SuppressWarnings("unchecked")
364     T canSerialize(final T object) {
365         byte[] byteArray = null;
366 
367         // Serialize data
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         // Deserialize Data
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      * Builds the value.
391      *
392      * @param <R>
393      *            the generic type
394      * @param returnType
395      *            the return type
396      * @param loadType
397      *            the load type
398      *
399      * @return the object
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      * Tests the equals/hashCode/toString methods of the specified class.
409      */
410     public void equalsHashCodeToStringSymmetricTest() {
411         // Run Equals Verifier
412         this.processEqualsVerifierSymmetricTest();
413 
414         // Create two new instances of the class under test and test equals, hashcode, and toString on them
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         // TODO 1/12/2019 JWL Internalize extension will require canEquals, equals, hashcode, and toString overrides.
420         /*
421          * try { this.extension = (Class<E>) new ExtensionBuilder<T>().generate(this.clazz); } catch (NotFoundException
422          * e) { Assert.fail(e.getMessage()); } catch (CannotCompileException e) { Assert.fail(e.getMessage()); }
423          */
424 
425         // If there is an extension, create an instance and test equals, hashcode, and toString on it against the empty
426         // class instance
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      * Process equals verifier symmetric test.
435      */
436     private void processEqualsVerifierSymmetricTest() {
437         // Run Equals Verifier
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      * Process class equals hash code to string symmetric test.
447      *
448      * @param x
449      *            the x
450      * @param y
451      *            the y
452      */
453     private void processClassEqualsHashCodeToStringSymmetricTest(final T x, final T y) {
454         // Verify that the objects were created successfully before testing equals, hashcode, and toString
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         // Test Equals, HashCode, and ToString on Empty Objects
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         // Test One Sided Tests on Empty Objects
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         // If the class has setters, the previous tests would have been against empty classes
475         // If so, load the classes and re-test
476         if (this.classHasSetters(this.clazz)) {
477             // Populate Side X
478             JavaBeanTesterWorker.load(this.clazz, x, this.loadData);
479 
480             // ReTest Equals (flip)
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             // Populate Side Y
486             JavaBeanTesterWorker.load(this.clazz, y, this.loadData);
487 
488             // ReTest Equals and HashCode
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                 // Part of the data loaded may be the same, so we cannot guarantee that equals and hashcode will be
498                 // different, but we can at least check that they are not both the same
499                 // Assertions.assertNotEquals(x, y);
500                 // Assertions.assertNotEquals(x.hashCode(), y.hashCode());
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         // Create Immutable Instance
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      * Process extension equals hash code to string symmetric test.
528      *
529      * @param y
530      *            the y
531      * @param ext
532      *            the ext
533      */
534     private void processExtensionEqualsHashCodeToStringSymmetricTest(final T y, final E ext) {
535         // Verify that the objects were created successfully before testing equals, hashcode, and toString
536         Assertions.assertNotNull(ext,
537                 String.format("Create new instance of extension %s resulted in null", this.extension.getName()));
538 
539         // Test Extension Equals, HashCode, and ToString on Empty Objects
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         // Test Extension One Sided Tests on Empty Objects
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         // If the class has setters, the previous tests would have been against empty classes
557         // If so, load the classes and re-test
558         if (this.classHasSetters(this.clazz)) {
559             // Populate Extension Side Ext
560             JavaBeanTesterWorker.load(this.extension, ext, this.loadData);
561 
562             // ReTest Extension Equals (flip)
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             // Populate Side Y
568             JavaBeanTesterWorker.load(this.clazz, y, this.loadData);
569 
570             // ReTest Extension Equals, HashCode, and ToString
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         // If class is final, use Object.class for comparison needs
585         if (Modifier.isFinal(clazz.getModifiers())) {
586             JavaBeanTesterWorker.logger.trace("Final object does not go through final equals check");
587             return;
588         }
589 
590         // Create Extension Immutable Instance
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      * Equals Tests will traverse one object changing values until all have been tested against another object. This is
607      * done to effectively test all paths through equals.
608      *
609      * @param instance
610      *            the class instance under test.
611      * @param expected
612      *            the instance expected for tests.
613      */
614     void equalsTests(final T instance, final T expected) {
615 
616         // Perform hashCode test dependent on data coming in
617         // Assert.assertEquals(expected.hashCode(), instance.hashCode());
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             // Java Metro Bug Patch (Boolean Wrapper usage of 'is' possible
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                     // Do nothing
639                 }
640             }
641 
642             if (getter != null && setter != null) {
643                 // We have both a get and set method for this property
644                 final Class<?> returnType = getter.getReturnType();
645                 final Class<?>[] params = setter.getParameterTypes();
646 
647                 if (params.length == 1 && params[0] == returnType) {
648                     // The set method has 1 argument, which is of the same type as the return type of the get method, so
649                     // we can test this property
650                     try {
651                         // Save original value
652                         final Object original = getter.invoke(instance);
653 
654                         // Build a value of the correct type to be passed to the set method using alternate test
655                         Object value = valueBuilder.buildValue(returnType, LoadType.ALTERNATE_DATA);
656 
657                         // Call the set method, then check the same value comes back out of the get method
658                         setter.invoke(instance, value);
659 
660                         // Check equals depending on data
661                         if (instance.equals(expected)) {
662                             Assertions.assertEquals(expected, instance);
663                         } else {
664                             Assertions.assertNotEquals(expected, instance);
665                         }
666 
667                         // Build a value of the correct type to be passed to the set method using null test
668                         value = valueBuilder.buildValue(returnType, LoadType.NULL_DATA);
669 
670                         // Call the set method, then check the same value comes back out of the get method
671                         setter.invoke(instance, value);
672 
673                         // Check equals depending on data
674                         if (instance.equals(expected)) {
675                             Assertions.assertEquals(expected, instance);
676                         } else {
677                             Assertions.assertNotEquals(expected, instance);
678                         }
679 
680                         // Reset to original value
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      * Class has setters.
696      *
697      * @param clazz
698      *            the clazz
699      *
700      * @return true, if successful
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      * Gets the props.
709      *
710      * @param clazz
711      *            the clazz
712      *
713      * @return the props
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                 // Check the list of properties that we don't want to test
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 }