View Javadoc
1   /*
2    * JavaBean Tester (https://github.com/hazendaz/javabean-tester)
3    *
4    * Copyright 2012-2025 Hazendaz.
5    *
6    * All rights reserved. This program and the accompanying materials
7    * are made available under the terms of The Apache Software License,
8    * Version 2.0 which accompanies this distribution, and is available at
9    * http://www.apache.org/licenses/LICENSE-2.0.txt
10   *
11   * Contributors:
12   *     CodeBox (Rob Dawson).
13   *     Hazendaz (Jeremy Landis).
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   * The Class JavaBeanTesterWorker.
59   *
60   * @param <T>
61   *            the generic type
62   * @param <E>
63   *            the element type
64   */
65  @Data
66  class JavaBeanTesterWorker<T, E> {
67  
68      /** The Constant logger. */
69      private static final Logger logger = LoggerFactory.getLogger(JavaBeanTesterWorker.class);
70  
71      /** The check clear. */
72      private CheckClear checkClear;
73  
74      /** The check constructor. */
75      private CheckConstructor checkConstructor;
76  
77      /** The check equals. */
78      private CheckEquals checkEquals;
79  
80      /** The check serializable. */
81      private CheckSerialize checkSerializable;
82  
83      /** The load data. */
84      private LoadData loadData;
85  
86      /** The clazz. */
87      private final Class<T> clazz;
88  
89      /** The extension. */
90      private Class<E> extension;
91  
92      /** The skip strict serialize. */
93      private SkipStrictSerialize skipStrictSerializable;
94  
95      /** The skip these. */
96      private Set<String> skipThese = new HashSet<>();
97  
98      /**
99       * Instantiates a new java bean tester worker.
100      *
101      * @param newClazz
102      *            the clazz
103      */
104     JavaBeanTesterWorker(final Class<T> newClazz) {
105         this.clazz = newClazz;
106     }
107 
108     /**
109      * Instantiates a new java bean tester worker.
110      *
111      * @param newClazz
112      *            the clazz
113      * @param newExtension
114      *            the extension
115      */
116     JavaBeanTesterWorker(final Class<T> newClazz, final Class<E> newExtension) {
117         this.clazz = newClazz;
118         this.extension = newExtension;
119     }
120 
121     /**
122      * Tests the load methods of the specified class.
123      *
124      * @param <L>
125      *            the type parameter associated with the class under test.
126      * @param clazz
127      *            the class under test.
128      * @param instance
129      *            the instance of class under test.
130      * @param loadData
131      *            load recursively all underlying data objects.
132      * @param skipThese
133      *            the names of any properties that should not be tested.
134      *
135      * @return the java bean tester worker
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      * Tests the clear, get, set, equals, hashCode, toString, serializable, and constructor(s) methods of the specified
152      * class.
153      */
154     public void test() {
155 
156         // Test Getter/Setter
157         this.getterSetterTests(new ClassInstance<T>().newInstance(this.clazz));
158 
159         // Test Clear
160         if (this.checkClear != CheckClear.OFF) {
161             this.clearTest();
162         }
163 
164         // Test constructor
165         if (this.checkConstructor != CheckConstructor.OFF) {
166             this.constructorsTest();
167         }
168 
169         // Test Serializable (internally uses on/off/strict checks)
170         this.checkSerializableTest();
171 
172         // Test Equals
173         if (this.checkEquals == CheckEquals.ON) {
174             this.equalsHashCodeToStringSymmetricTest();
175         }
176 
177     }
178 
179     /**
180      * Getter Setter Tests.
181      *
182      * @param instance
183      *            the instance of class under test.
184      *
185      * @return the ter setter tests
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             // Java Metro Bug Patch (Boolean Wrapper usage of 'is' possible
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                     // Do nothing
200                 }
201             }
202 
203             if (getter != null && setter != null) {
204                 // We have both a get and set method for this property
205                 final Class<?> returnType = getter.getReturnType();
206                 final Class<?>[] params = setter.getParameterTypes();
207 
208                 if (params.length == 1 && params[0] == returnType) {
209                     // The set method has 1 argument, which is of the same type as the return type of the get method, so
210                     // we can test this property
211                     try {
212                         // Build a value of the correct type to be passed to the set method
213                         final Object value = this.buildValue(returnType, LoadType.STANDARD_DATA);
214 
215                         // Build an instance of the bean that we are testing (each property test gets a new instance)
216                         final T bean = new ClassInstance<T>().newInstance(this.clazz);
217 
218                         // Call the set method, then check the same value comes back out of the get method
219                         setter.invoke(bean, value);
220 
221                         // Use data set on instance
222                         setter.invoke(instance, value);
223 
224                         final Object expectedValue = value;
225                         Object actualValue = getter.invoke(bean);
226 
227                         // java.util.Date normalization patch
228                         //
229                         // Date is zero based so it adds 1 through normalization. Since we always pass '1' here, it is
230                         // the same as stating February. Thus we roll over the month quite often into March towards
231                         // end of the month resulting in '1' != '2' situation. The reason we pass '1' is that we are
232                         // testing the content of the object and have no idea it is a date to start with. It is simply
233                         // that it sees getters/setters and tries to load them appropriately. The underlying problem
234                         // with that is that the Date object performs normalization to avoid dates like 2-30 that do
235                         // not exist and is not a typical getter/setter use-case. It is also deprecated but we don't
236                         // want to simply skip all deprecated items as we intend to test as much as possible.
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      * Clear test.
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                     // Perform any Post Construction on object without parameters
270                     List<Annotation> annotations = null;
271                     for (final Method mt : methods) {
272                         annotations = Arrays.asList(mt.getAnnotations());
273                         for (final Annotation annotation : annotations) {
274                             // XXX On purpose logic change to support both javax and jakarta namespace for annotations
275                             if ("PostConstruct".equals(annotation.annotationType().getSimpleName())
276                                     && mt.getParameterTypes().length == 0) {
277                                 // Invoke method newClass
278                                 mt.invoke(newClass);
279                                 // Invoke method expectedClass
280                                 mt.invoke(expectedClass);
281                             }
282                         }
283                     }
284                     // Invoke clear only on newClass
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      * Constructors test.
299      */
300     void constructorsTest() {
301         for (final Constructor<?> constructor : this.clazz.getConstructors()) {
302 
303             // Skip deprecated constructors
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             // Load Data
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             // TODO 1/12/2019 JWL Add checking of new object properties
326         }
327     }
328 
329     /**
330      * Check Serializable test.
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             // Toggle to throw or not throw error with only one way working
337             if (this.skipStrictSerializable != SkipStrictSerialize.ON) {
338                 Assertions.assertEquals(object, newObject);
339             } else {
340                 Assertions.assertNotEquals(object, newObject);
341             }
342             return;
343         }
344 
345         // Only throw error when specifically checking on serialization
346         if (this.checkSerializable == CheckSerialize.ON) {
347             Assertions.fail(String.format("Class is not serializable '%s'", object.getClass().getName()));
348         }
349     }
350 
351     /**
352      * Implements serializable.
353      *
354      * @param object
355      *            the object
356      *
357      * @return true, if successful
358      */
359     boolean implementsSerializable(final T object) {
360         return object instanceof Externalizable || object instanceof Serializable;
361     }
362 
363     /**
364      * Can serialize.
365      *
366      * @param object
367      *            the object
368      *
369      * @return object read after serialization
370      */
371     @SuppressWarnings("unchecked")
372     T canSerialize(final T object) {
373         byte[] byteArray = null;
374 
375         // Serialize data
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         // Deserialize Data
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      * Builds the value.
399      *
400      * @param <R>
401      *            the generic type
402      * @param returnType
403      *            the return type
404      * @param loadType
405      *            the load type
406      *
407      * @return the object
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      * Tests the equals/hashCode/toString methods of the specified class.
417      */
418     public void equalsHashCodeToStringSymmetricTest() {
419         // Run Equals Verifier
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         // Create Instances
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         // TODO 1/12/2019 JWL Internalize extension will require canEquals, equals, hashcode, and toString overrides.
436         /*
437          * try { this.extension = (Class<E>) new ExtensionBuilder<T>().generate(this.clazz); } catch (NotFoundException
438          * e) { Assert.fail(e.getMessage()); } catch (CannotCompileException e) { Assert.fail(e.getMessage()); }
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         // Test Equals, HashCode, and ToString on Empty Objects
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         // Test Extension Equals, HashCode, and ToString on Empty Objects
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         // Test One Sided Tests on Empty Objects
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         // Test Extension One Sided Tests on Empty Objects
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         // If the class has setters, the previous tests would have been against empty classes
477         // If so, load the classes and re-test
478         if (this.classHasSetters(this.clazz)) {
479             // Populate Side X
480             JavaBeanTesterWorker.load(this.clazz, x, this.loadData);
481 
482             // Populate Extension Side Ext
483             JavaBeanTesterWorker.load(this.extension, ext, this.loadData);
484 
485             // ReTest Equals (flip)
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             // ReTest Extension Equals (flip)
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             // Populate Size Y
496             JavaBeanTesterWorker.load(this.clazz, y, this.loadData);
497 
498             // ReTest Equals and HashCode
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             // ReTest Extension Equals, HashCode, and ToString
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         // Create Immutable Instance
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         // If class is final, use Object.class for comparison needs
540         if (Modifier.isFinal(clazz.getModifiers())) {
541             JavaBeanTesterWorker.logger.trace("Final object does not go through final equals check");
542             return;
543         }
544 
545         // Create Extension Immutable Instance
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      * Equals Tests will traverse one object changing values until all have been tested against another object. This is
562      * done to effectively test all paths through equals.
563      *
564      * @param instance
565      *            the class instance under test.
566      * @param expected
567      *            the instance expected for tests.
568      */
569     void equalsTests(final T instance, final T expected) {
570 
571         // Perform hashCode test dependent on data coming in
572         // Assert.assertEquals(expected.hashCode(), instance.hashCode());
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             // Java Metro Bug Patch (Boolean Wrapper usage of 'is' possible
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                     // Do nothing
594                 }
595             }
596 
597             if (getter != null && setter != null) {
598                 // We have both a get and set method for this property
599                 final Class<?> returnType = getter.getReturnType();
600                 final Class<?>[] params = setter.getParameterTypes();
601 
602                 if (params.length == 1 && params[0] == returnType) {
603                     // The set method has 1 argument, which is of the same type as the return type of the get method, so
604                     // we can test this property
605                     try {
606                         // Save original value
607                         final Object original = getter.invoke(instance);
608 
609                         // Build a value of the correct type to be passed to the set method using alternate test
610                         Object value = valueBuilder.buildValue(returnType, LoadType.ALTERNATE_DATA);
611 
612                         // Call the set method, then check the same value comes back out of the get method
613                         setter.invoke(instance, value);
614 
615                         // Check equals depending on data
616                         if (instance.equals(expected)) {
617                             Assertions.assertEquals(expected, instance);
618                         } else {
619                             Assertions.assertNotEquals(expected, instance);
620                         }
621 
622                         // Build a value of the correct type to be passed to the set method using null test
623                         value = valueBuilder.buildValue(returnType, LoadType.NULL_DATA);
624 
625                         // Call the set method, then check the same value comes back out of the get method
626                         setter.invoke(instance, value);
627 
628                         // Check equals depending on data
629                         if (instance.equals(expected)) {
630                             Assertions.assertEquals(expected, instance);
631                         } else {
632                             Assertions.assertNotEquals(expected, instance);
633                         }
634 
635                         // Reset to original value
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      * Class has setters.
651      *
652      * @param clazz
653      *            the clazz
654      *
655      * @return true, if successful
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      * Gets the props.
664      *
665      * @param clazz
666      *            the clazz
667      *
668      * @return the props
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                 // Check the list of properties that we don't want to test
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 }