View Javadoc
1   /*
2    * JavaBean Tester (https://github.com/hazendaz/javabean-tester)
3    *
4    * Copyright 2012-2023 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.util.ArrayList;
41  import java.util.Arrays;
42  import java.util.Date;
43  import java.util.HashSet;
44  import java.util.List;
45  import java.util.Set;
46  
47  import lombok.Data;
48  
49  import net.sf.cglib.beans.BeanCopier;
50  
51  import nl.jqno.equalsverifier.EqualsVerifier;
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 Serializable || object instanceof Externalizable;
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).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 BeanCopier clazzBeanCopier = BeanCopier.create(this.clazz, this.clazz, true);
525             final T e = new ClassInstance<T>().newInstance(this.clazz);
526             clazzBeanCopier.copy(x, e, null);
527             Assertions.assertEquals(e, x);
528         } catch (final Exception e) {
529             JavaBeanTesterWorker.LOGGER.trace("Do nothing class is not mutable", e);
530         }
531 
532         // Create Extension Immutable Instance
533         try {
534             final BeanCopier extensionBeanCopier = BeanCopier.create(this.extension, this.extension, true);
535             final E e = new ClassInstance<E>().newInstance(this.extension);
536             extensionBeanCopier.copy(ext, e, null);
537             Assertions.assertEquals(e, ext);
538         } catch (final Exception e) {
539             JavaBeanTesterWorker.LOGGER.trace("Do nothing class is not mutable", e);
540         }
541     }
542 
543     /**
544      * Equals Tests will traverse one object changing values until all have been tested against another object. This is
545      * done to effectively test all paths through equals.
546      *
547      * @param instance
548      *            the class instance under test.
549      * @param expected
550      *            the instance expected for tests.
551      */
552     void equalsTests(final T instance, final T expected) {
553 
554         // Perform hashCode test dependent on data coming in
555         // Assert.assertEquals(expected.hashCode(), instance.hashCode());
556         if (expected.hashCode() == instance.hashCode()) {
557             Assertions.assertEquals(expected.hashCode(), instance.hashCode());
558         } else {
559             Assertions.assertNotEquals(expected.hashCode(), instance.hashCode());
560         }
561 
562         final ValueBuilder valueBuilder = new ValueBuilder();
563         valueBuilder.setLoadData(this.loadData);
564 
565         final PropertyDescriptor[] props = this.getProps(instance.getClass());
566         for (final PropertyDescriptor prop : props) {
567             Method getter = prop.getReadMethod();
568             final Method setter = prop.getWriteMethod();
569 
570             // Java Metro Bug Patch (Boolean Wrapper usage of 'is' possible
571             if (getter == null && setter != null) {
572                 final String isBooleanWrapper = "is" + setter.getName().substring(3);
573                 try {
574                     getter = this.clazz.getMethod(isBooleanWrapper);
575                 } catch (NoSuchMethodException | SecurityException e) {
576                     // Do nothing
577                 }
578             }
579 
580             if (getter != null && setter != null) {
581                 // We have both a get and set method for this property
582                 final Class<?> returnType = getter.getReturnType();
583                 final Class<?>[] params = setter.getParameterTypes();
584 
585                 if (params.length == 1 && params[0] == returnType) {
586                     // The set method has 1 argument, which is of the same type as the return type of the get method, so
587                     // we can test this property
588                     try {
589                         // Save original value
590                         final Object original = getter.invoke(instance);
591 
592                         // Build a value of the correct type to be passed to the set method using alternate test
593                         Object value = valueBuilder.buildValue(returnType, LoadType.ALTERNATE_DATA);
594 
595                         // Call the set method, then check the same value comes back out of the get method
596                         setter.invoke(instance, value);
597 
598                         // Check equals depending on data
599                         if (instance.equals(expected)) {
600                             Assertions.assertEquals(expected, instance);
601                         } else {
602                             Assertions.assertNotEquals(expected, instance);
603                         }
604 
605                         // Build a value of the correct type to be passed to the set method using null test
606                         value = valueBuilder.buildValue(returnType, LoadType.NULL_DATA);
607 
608                         // Call the set method, then check the same value comes back out of the get method
609                         setter.invoke(instance, value);
610 
611                         // Check equals depending on data
612                         if (instance.equals(expected)) {
613                             Assertions.assertEquals(expected, instance);
614                         } else {
615                             Assertions.assertNotEquals(expected, instance);
616                         }
617 
618                         // Reset to original value
619                         setter.invoke(instance, original);
620 
621                     } catch (final IllegalAccessException | IllegalArgumentException | InvocationTargetException
622                             | SecurityException e) {
623                         Assertions.fail(
624                                 String.format("An exception was thrown while testing the property (equals) '%s': '%s'",
625                                         prop.getName(), e.toString()));
626                     }
627                 }
628             }
629         }
630     }
631 
632     /**
633      * Class has setters.
634      *
635      * @param clazz
636      *            the clazz
637      *
638      * @return true, if successful
639      */
640     private boolean classHasSetters(final Class<T> clazz) {
641         return Arrays.stream(this.getProps(clazz))
642                 .anyMatch(propertyDescriptor -> propertyDescriptor.getWriteMethod() != null);
643     }
644 
645     /**
646      * Gets the props.
647      *
648      * @param clazz
649      *            the clazz
650      *
651      * @return the props
652      */
653     private PropertyDescriptor[] getProps(final Class<?> clazz) {
654         try {
655             final List<PropertyDescriptor> usedProps = new ArrayList<>(
656                     Introspector.getBeanInfo(clazz).getPropertyDescriptors().length);
657             final List<PropertyDescriptor> props = Arrays
658                     .asList(Introspector.getBeanInfo(clazz).getPropertyDescriptors());
659             nextProp: for (final PropertyDescriptor prop : props) {
660                 // Check the list of properties that we don't want to test
661                 for (final String skipThis : this.skipThese) {
662                     if (skipThis.equals(prop.getName())) {
663                         continue nextProp;
664                     }
665                 }
666                 usedProps.add(prop);
667             }
668             return usedProps.toArray(new PropertyDescriptor[usedProps.size()]);
669         } catch (final IntrospectionException e) {
670             Assertions.fail(String.format("An exception was thrown while testing class '%s': '%s'",
671                     this.clazz.getName(), e.toString()));
672             return new PropertyDescriptor[0];
673         }
674     }
675 
676 }