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         // Serialize data
374         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
375         try {
376             new ObjectOutputStream(baos).writeObject(object);
377         } catch (final IOException e) {
378             Assertions.fail(String.format("An exception was thrown while serializing the class '%s': '%s',",
379                     object.getClass().getName(), e.toString()));
380             return null;
381         }
382 
383         // Deserialize Data
384         final ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
385         try {
386             return (T) new ObjectInputStream(bais).readObject();
387         } catch (final ClassNotFoundException | IOException e) {
388             Assertions.fail(String.format("An exception was thrown while deserializing the class '%s': '%s',",
389                     object.getClass().getName(), e.toString()));
390         }
391         return null;
392     }
393 
394     /**
395      * Builds the value.
396      *
397      * @param <R>
398      *            the generic type
399      * @param returnType
400      *            the return type
401      * @param loadType
402      *            the load type
403      *
404      * @return the object
405      */
406     private <R> Object buildValue(final Class<R> returnType, final LoadType loadType) {
407         final ValueBuilder valueBuilder = new ValueBuilder();
408         valueBuilder.setLoadData(this.loadData);
409         return valueBuilder.buildValue(returnType, loadType);
410     }
411 
412     /**
413      * Tests the equals/hashCode/toString methods of the specified class.
414      */
415     public void equalsHashCodeToStringSymmetricTest() {
416         // Run Equals Verifier
417         try {
418             EqualsVerifier.simple().forClass(this.clazz).suppress(Warning.BIGDECIMAL_EQUALITY).verify();
419         } catch (AssertionError e) {
420             JavaBeanTesterWorker.LOGGER.warn("EqualsVerifier attempt failed: {}", e.getMessage());
421         }
422 
423         // Create Instances
424         final T x = new ClassInstance<T>().newInstance(this.clazz);
425         final T y = new ClassInstance<T>().newInstance(this.clazz);
426 
427         Assertions.assertNotNull(x,
428                 String.format("Create new instance of class '%s' resulted in null", this.clazz.getName()));
429         Assertions.assertNotNull(y,
430                 String.format("Create new instance of class '%s' resulted in null", this.clazz.getName()));
431 
432         // TODO 1/12/2019 JWL Internalize extension will require canEquals, equals, hashcode, and toString overrides.
433         /*
434          * try { this.extension = (Class<E>) new ExtensionBuilder<T>().generate(this.clazz); } catch (NotFoundException
435          * e) { Assert.fail(e.getMessage()); } catch (CannotCompileException e) { Assert.fail(e.getMessage()); }
436          */
437         final E ext = new ClassInstance<E>().newInstance(this.extension);
438 
439         Assertions.assertNotNull(ext,
440                 String.format("Create new instance of extension %s resulted in null", this.extension.getName()));
441 
442         // Test Equals, HashCode, and ToString on Empty Objects
443         Assertions.assertEquals(x, y,
444                 String.format(".equals() should be consistent for two empty objects of type %s", this.clazz.getName()));
445         Assertions.assertEquals(x.hashCode(), y.hashCode(), String
446                 .format(".hashCode() should be consistent for two empty objects of type %s", this.clazz.getName()));
447         Assertions.assertEquals(x.toString(), y.toString(), String
448                 .format(".toString() should be consistent for two empty objects of type %s", this.clazz.getName()));
449 
450         // Test Extension Equals, HashCode, and ToString on Empty Objects
451         Assertions.assertNotEquals(ext, y,
452                 String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
453                         this.extension.getName(), this.clazz.getName()));
454         Assertions.assertNotEquals(ext.hashCode(), y.hashCode(),
455                 String.format(".hashCode() should not be equal for extension of type %s and empty object of type %s",
456                         this.extension.getName(), this.clazz.getName()));
457         Assertions.assertNotEquals(ext.toString(), y.toString(),
458                 String.format(".toString() should not be equal for extension of type %s and empty object of type %s",
459                         this.extension.getName(), this.clazz.getName()));
460 
461         // Test One Sided Tests on Empty Objects
462         Assertions.assertNotEquals(x, null,
463                 String.format("An empty object of type %s should not be equal to null", this.clazz.getName()));
464         Assertions.assertEquals(x, x,
465                 String.format("An empty object of type %s should be equal to itself", this.clazz.getName()));
466 
467         // Test Extension One Sided Tests on Empty Objects
468         Assertions.assertNotEquals(ext, null,
469                 String.format("An empty extension of type %s should not be equal to null", this.clazz.getName()));
470         Assertions.assertEquals(ext, ext,
471                 String.format("An empty extension of type %s should be equal to itself", this.extension.getName()));
472 
473         // If the class has setters, the previous tests would have been against empty classes
474         // If so, load the classes and re-test
475         if (this.classHasSetters(this.clazz)) {
476             // Populate Side X
477             JavaBeanTesterWorker.load(this.clazz, x, this.loadData);
478 
479             // Populate Extension Side Ext
480             JavaBeanTesterWorker.load(this.extension, ext, this.loadData);
481 
482             // ReTest Equals (flip)
483             Assertions.assertNotEquals(y, x,
484                     String.format(".equals() should not be consistent for one empty and one loaded object of type %s",
485                             this.clazz.getName()));
486 
487             // ReTest Extension Equals (flip)
488             Assertions.assertNotEquals(y, ext,
489                     String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
490                             this.extension.getName(), this.clazz.getName()));
491 
492             // Populate Size Y
493             JavaBeanTesterWorker.load(this.clazz, y, this.loadData);
494 
495             // ReTest Equals and HashCode
496             if (this.loadData == LoadData.ON) {
497                 Assertions.assertEquals(x, y,
498                         String.format(".equals() should be equal for two instances of type %s with loaded data",
499                                 this.clazz.getName()));
500                 Assertions.assertEquals(x.hashCode(), y.hashCode(),
501                         String.format(".hashCode() should be equal for two instances of type %s with loaded data",
502                                 this.clazz.getName()));
503             } else {
504                 Assertions.assertNotEquals(x, y);
505                 Assertions.assertNotEquals(x.hashCode(), y.hashCode());
506             }
507 
508             // ReTest Extension Equals, HashCode, and ToString
509             Assertions.assertNotEquals(ext, y,
510                     String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
511                             this.extension.getName(), this.clazz.getName()));
512             Assertions.assertNotEquals(ext.hashCode(), y.hashCode(),
513                     String.format(
514                             ".hashCode() should not be equal for extension of type %s and empty object of type %s",
515                             this.extension.getName(), this.clazz.getName()));
516             Assertions.assertNotEquals(ext.toString(), y.toString(),
517                     String.format(
518                             ".toString() should not be equal for extension of type %s and empty object of type %s",
519                             this.extension.getName(), this.clazz.getName()));
520         }
521 
522         // Create Immutable Instance
523         try {
524             final T e = new ClassInstance<T>().newInstance(this.clazz);
525             ByteBuddyBeanCopier.copy(x, e, (value, targetType) -> {
526                 if (targetType == boolean.class) {
527                     return value == null ? Boolean.FALSE : value;
528                 }
529                 return value;
530             });
531             Assertions.assertEquals(e, x);
532         } catch (final Exception e) {
533             JavaBeanTesterWorker.LOGGER.trace("Do nothing class is not mutable", e);
534         }
535 
536         // If class is final, use Object.class for comparison needs
537         if (Modifier.isFinal(clazz.getModifiers())) {
538             JavaBeanTesterWorker.LOGGER.trace("Final object does not go through final equals check");
539             return;
540         }
541 
542         // Create Extension Immutable Instance
543         try {
544             final E e = new ClassInstance<E>().newInstance(this.extension);
545             ByteBuddyBeanCopier.copy(ext, e, (value, targetType) -> {
546                 if (targetType == boolean.class) {
547                     return value == null ? Boolean.FALSE : value;
548                 }
549                 return value;
550             });
551             Assertions.assertEquals(e, ext);
552         } catch (final Exception e) {
553             JavaBeanTesterWorker.LOGGER.trace("Do nothing class is not mutable", e);
554         }
555     }
556 
557     /**
558      * Equals Tests will traverse one object changing values until all have been tested against another object. This is
559      * done to effectively test all paths through equals.
560      *
561      * @param instance
562      *            the class instance under test.
563      * @param expected
564      *            the instance expected for tests.
565      */
566     void equalsTests(final T instance, final T expected) {
567 
568         // Perform hashCode test dependent on data coming in
569         // Assert.assertEquals(expected.hashCode(), instance.hashCode());
570         if (expected.hashCode() == instance.hashCode()) {
571             Assertions.assertEquals(expected.hashCode(), instance.hashCode());
572         } else {
573             Assertions.assertNotEquals(expected.hashCode(), instance.hashCode());
574         }
575 
576         final ValueBuilder valueBuilder = new ValueBuilder();
577         valueBuilder.setLoadData(this.loadData);
578 
579         final PropertyDescriptor[] props = this.getProps(instance.getClass());
580         for (final PropertyDescriptor prop : props) {
581             Method getter = prop.getReadMethod();
582             final Method setter = prop.getWriteMethod();
583 
584             // Java Metro Bug Patch (Boolean Wrapper usage of 'is' possible
585             if (getter == null && setter != null) {
586                 final String isBooleanWrapper = "is" + setter.getName().substring(3);
587                 try {
588                     getter = this.clazz.getMethod(isBooleanWrapper);
589                 } catch (NoSuchMethodException | SecurityException e) {
590                     // Do nothing
591                 }
592             }
593 
594             if (getter != null && setter != null) {
595                 // We have both a get and set method for this property
596                 final Class<?> returnType = getter.getReturnType();
597                 final Class<?>[] params = setter.getParameterTypes();
598 
599                 if (params.length == 1 && params[0] == returnType) {
600                     // The set method has 1 argument, which is of the same type as the return type of the get method, so
601                     // we can test this property
602                     try {
603                         // Save original value
604                         final Object original = getter.invoke(instance);
605 
606                         // Build a value of the correct type to be passed to the set method using alternate test
607                         Object value = valueBuilder.buildValue(returnType, LoadType.ALTERNATE_DATA);
608 
609                         // Call the set method, then check the same value comes back out of the get method
610                         setter.invoke(instance, value);
611 
612                         // Check equals depending on data
613                         if (instance.equals(expected)) {
614                             Assertions.assertEquals(expected, instance);
615                         } else {
616                             Assertions.assertNotEquals(expected, instance);
617                         }
618 
619                         // Build a value of the correct type to be passed to the set method using null test
620                         value = valueBuilder.buildValue(returnType, LoadType.NULL_DATA);
621 
622                         // Call the set method, then check the same value comes back out of the get method
623                         setter.invoke(instance, value);
624 
625                         // Check equals depending on data
626                         if (instance.equals(expected)) {
627                             Assertions.assertEquals(expected, instance);
628                         } else {
629                             Assertions.assertNotEquals(expected, instance);
630                         }
631 
632                         // Reset to original value
633                         setter.invoke(instance, original);
634 
635                     } catch (final IllegalAccessException | IllegalArgumentException | InvocationTargetException
636                             | SecurityException e) {
637                         Assertions.fail(
638                                 String.format("An exception was thrown while testing the property (equals) '%s': '%s'",
639                                         prop.getName(), e.toString()));
640                     }
641                 }
642             }
643         }
644     }
645 
646     /**
647      * Class has setters.
648      *
649      * @param clazz
650      *            the clazz
651      *
652      * @return true, if successful
653      */
654     private boolean classHasSetters(final Class<T> clazz) {
655         return Arrays.stream(this.getProps(clazz))
656                 .anyMatch(propertyDescriptor -> propertyDescriptor.getWriteMethod() != null);
657     }
658 
659     /**
660      * Gets the props.
661      *
662      * @param clazz
663      *            the clazz
664      *
665      * @return the props
666      */
667     private PropertyDescriptor[] getProps(final Class<?> clazz) {
668         try {
669             final List<PropertyDescriptor> usedProps = new ArrayList<>(
670                     Introspector.getBeanInfo(clazz).getPropertyDescriptors().length);
671             final List<PropertyDescriptor> props = Arrays
672                     .asList(Introspector.getBeanInfo(clazz).getPropertyDescriptors());
673             nextProp: for (final PropertyDescriptor prop : props) {
674                 // Check the list of properties that we don't want to test
675                 for (final String skipThis : this.skipThese) {
676                     if (skipThis.equals(prop.getName())) {
677                         continue nextProp;
678                     }
679                 }
680                 usedProps.add(prop);
681             }
682             return usedProps.toArray(new PropertyDescriptor[usedProps.size()]);
683         } catch (final IntrospectionException e) {
684             Assertions.fail(String.format("An exception was thrown while testing class '%s': '%s'",
685                     this.clazz.getName(), e.toString()));
686             return new PropertyDescriptor[0];
687         }
688     }
689 
690 }