• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hazendaz / javabean-tester / 3227

16 Feb 2026 12:20AM UTC coverage: 87.5% (-0.4%) from 87.852%
3227

push

github

hazendaz
Found possible bug in handling, its not always possible to be purist on current code for equals

noted what the expectation was, its not achieved, so just allow it through.  This was an existing problem that the tests didn't catch due to sub objects.  I had added a test when looking at lombok builder pattern and stumbled onto it.  In the equals only tests, its actually doing almost same thing.  This area is known not strong enough and is additionally using equals verifier to try to compensate.

285 of 331 branches covered (86.1%)

5 of 5 new or added lines in 1 file covered. (100.0%)

67 existing lines in 6 files now uncovered.

525 of 600 relevant lines covered (87.5%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.13
/src/main/java/com/codebox/bean/JavaBeanTesterWorker.java
1
/*
2
 * SPDX-License-Identifier: Apache-2.0
3
 * See LICENSE file for details.
4
 *
5
 * Copyright 2012-2026 hazendaz
6
 *
7
 * Portions of initial baseline code (getter/setter test) by Rob Dawson (CodeBox)
8
 */
9
package com.codebox.bean;
10

11
import com.codebox.enums.CheckClear;
12
import com.codebox.enums.CheckConstructor;
13
import com.codebox.enums.CheckEquals;
14
import com.codebox.enums.CheckSerialize;
15
import com.codebox.enums.LoadData;
16
import com.codebox.enums.LoadType;
17
import com.codebox.enums.SkipStrictSerialize;
18
import com.codebox.instance.ClassInstance;
19

20
import java.beans.IntrospectionException;
21
import java.beans.Introspector;
22
import java.beans.PropertyDescriptor;
23
import java.io.ByteArrayInputStream;
24
import java.io.ByteArrayOutputStream;
25
import java.io.Externalizable;
26
import java.io.IOException;
27
import java.io.ObjectInputStream;
28
import java.io.ObjectOutputStream;
29
import java.io.Serializable;
30
import java.lang.annotation.Annotation;
31
import java.lang.reflect.Constructor;
32
import java.lang.reflect.InvocationTargetException;
33
import java.lang.reflect.Method;
34
import java.lang.reflect.Modifier;
35
import java.util.ArrayList;
36
import java.util.Arrays;
37
import java.util.Date;
38
import java.util.HashSet;
39
import java.util.List;
40
import java.util.Set;
41

42
import lombok.Data;
43

44
import nl.jqno.equalsverifier.EqualsVerifier;
45
import nl.jqno.equalsverifier.Warning;
46

47
import org.junit.jupiter.api.Assertions;
48
import org.slf4j.Logger;
49
import org.slf4j.LoggerFactory;
50

51
/**
52
 * The Class JavaBeanTesterWorker.
53
 *
54
 * @param <T>
55
 *            the generic type
56
 * @param <E>
57
 *            the element type
58
 */
59
@Data
60
class JavaBeanTesterWorker<T, E> {
61

62
    /** The Constant logger. */
63
    private static final Logger logger = LoggerFactory.getLogger(JavaBeanTesterWorker.class);
1✔
64

65
    /** The check clear. */
66
    private CheckClear checkClear;
67

68
    /** The check constructor. */
69
    private CheckConstructor checkConstructor;
70

71
    /** The check equals. */
72
    private CheckEquals checkEquals;
73

74
    /** The check serializable. */
75
    private CheckSerialize checkSerializable;
76

77
    /** The load data. */
78
    private LoadData loadData;
79

80
    /** The clazz. */
81
    private final Class<T> clazz;
82

83
    /** The extension. */
84
    private Class<E> extension;
85

86
    /** The skip strict serialize. */
87
    private SkipStrictSerialize skipStrictSerializable;
88

89
    /** The skip these. */
90
    private Set<String> skipThese = new HashSet<>();
1✔
91

92
    /**
93
     * Instantiates a new java bean tester worker.
94
     *
95
     * @param newClazz
96
     *            the clazz
97
     */
98
    JavaBeanTesterWorker(final Class<T> newClazz) {
1✔
99
        this.clazz = newClazz;
1✔
100
    }
1✔
101

102
    /**
103
     * Instantiates a new java bean tester worker.
104
     *
105
     * @param newClazz
106
     *            the clazz
107
     * @param newExtension
108
     *            the extension
109
     */
110
    JavaBeanTesterWorker(final Class<T> newClazz, final Class<E> newExtension) {
1✔
111
        this.clazz = newClazz;
1✔
112
        this.extension = newExtension;
1✔
113
    }
1✔
114

115
    /**
116
     * Tests the load methods of the specified class.
117
     *
118
     * @param <L>
119
     *            the type parameter associated with the class under test.
120
     * @param clazz
121
     *            the class under test.
122
     * @param instance
123
     *            the instance of class under test.
124
     * @param loadData
125
     *            load recursively all underlying data objects.
126
     * @param skipThese
127
     *            the names of any properties that should not be tested.
128
     *
129
     * @return the java bean tester worker
130
     */
131
    public static <L> JavaBeanTesterWorker<L, Object> load(final Class<L> clazz, final L instance,
132
            final LoadData loadData, final String... skipThese) {
133
        final JavaBeanTesterWorker<L, Object> worker = new JavaBeanTesterWorker<>(clazz);
1✔
134

135
        worker.setLoadData(loadData);
1✔
136
        if (skipThese != null) {
1!
137
            worker.setSkipThese(new HashSet<>(Arrays.asList(skipThese)));
1✔
138
        }
139
        worker.getterSetterTests(instance);
1✔
140

141
        return worker;
1✔
142
    }
143

144
    /**
145
     * Tests the clear, get, set, equals, hashCode, toString, serializable, and constructor(s) methods of the specified
146
     * class.
147
     */
148
    public void test() {
149

150
        // Test Getter/Setter
151
        this.getterSetterTests(new ClassInstance<T>().newInstance(this.clazz));
1✔
152

153
        // Test Clear
154
        if (this.checkClear != CheckClear.OFF) {
1✔
155
            this.clearTest();
1✔
156
        }
157

158
        // Test constructor
159
        if (this.checkConstructor != CheckConstructor.OFF) {
1✔
160
            this.constructorsTest();
1✔
161
        }
162

163
        // Test Serializable (internally uses on/off/strict checks)
164
        this.checkSerializableTest();
1✔
165

166
        // Test Equals
167
        if (this.checkEquals == CheckEquals.ON) {
1✔
168
            this.equalsHashCodeToStringSymmetricTest();
1✔
169
        }
170

171
    }
1✔
172

173
    /**
174
     * Getter Setter Tests.
175
     *
176
     * @param instance
177
     *            the instance of class under test.
178
     */
179
    void getterSetterTests(final T instance) {
180
        final PropertyDescriptor[] props = this.getProps(this.clazz);
1✔
181
        for (final PropertyDescriptor prop : props) {
1✔
182
            Method getter = prop.getReadMethod();
1✔
183
            final Method setter = prop.getWriteMethod();
1✔
184

185
            // Java Metro Bug Patch (Boolean Wrapper usage of 'is' possible
186
            if (getter == null && setter != null) {
1!
187
                final String isBooleanWrapper = "is" + setter.getName().substring(3);
1✔
188
                try {
189
                    getter = this.clazz.getMethod(isBooleanWrapper);
1✔
190
                } catch (NoSuchMethodException | SecurityException e) {
1✔
191
                    // Do nothing
192
                }
1✔
193
            }
194

195
            if (getter != null && setter != null) {
1✔
196
                // We have both a get and set method for this property
197
                final Class<?> returnType = getter.getReturnType();
1✔
198
                final Class<?>[] params = setter.getParameterTypes();
1✔
199

200
                if (params.length == 1 && params[0] == returnType) {
1!
201
                    // The set method has 1 argument, which is of the same type as the return type of the get method, so
202
                    // we can test this property
203
                    try {
204
                        // Build a value of the correct type to be passed to the set method
205
                        final Object value = this.buildValue(returnType, LoadType.STANDARD_DATA);
1✔
206

207
                        // Build an instance of the bean that we are testing (each property test gets a new instance)
208
                        final T bean = new ClassInstance<T>().newInstance(this.clazz);
1✔
209

210
                        // Call the set method, then check the same value comes back out of the get method
211
                        setter.invoke(bean, value);
1✔
212

213
                        // Use data set on instance
214
                        setter.invoke(instance, value);
1✔
215

216
                        final Object expectedValue = value;
1✔
217
                        Object actualValue = getter.invoke(bean);
1✔
218

219
                        // java.util.Date normalization patch
220
                        //
221
                        // Date is zero based so it adds 1 through normalization. Since we always pass '1' here, it is
222
                        // the same as stating February. Thus we roll over the month quite often into March towards
223
                        // end of the month resulting in '1' != '2' situation. The reason we pass '1' is that we are
224
                        // testing the content of the object and have no idea it is a date to start with. It is simply
225
                        // that it sees getters/setters and tries to load them appropriately. The underlying problem
226
                        // with that is that the Date object performs normalization to avoid dates like 2-30 that do
227
                        // not exist and is not a typical getter/setter use-case. It is also deprecated but we don't
228
                        // want to simply skip all deprecated items as we intend to test as much as possible.
229
                        //
230
                        if (this.clazz == Date.class && prop.getName().equals("month")
1✔
231
                                && expectedValue.equals(Integer.valueOf("1"))
1!
232
                                && actualValue.equals(Integer.valueOf("2"))) {
1!
UNCOV
233
                            actualValue = Integer.valueOf("1");
×
234
                        }
235

236
                        Assertions.assertEquals(expectedValue, actualValue,
1✔
237
                                String.format("Failed while testing property '%s' of class '%s'", prop.getName(),
1✔
238
                                        this.clazz.getName()));
1✔
239

UNCOV
240
                    } catch (final IllegalAccessException | IllegalArgumentException | InvocationTargetException
×
241
                            | SecurityException e) {
UNCOV
242
                        Assertions.fail(String.format(
×
243
                                "An exception was thrown while testing class '%s' with the property (getter/setter) '%s': '%s'",
UNCOV
244
                                this.clazz.getName(), prop.getName(), e.toString()));
×
245
                    }
1✔
246
                }
247
            }
248
        }
249
    }
1✔
250

251
    /**
252
     * Clear test.
253
     */
254
    void clearTest() {
255
        final Method[] methods = this.clazz.getDeclaredMethods();
1✔
256
        for (final Method method : methods) {
1✔
257
            if (method.getName().equals("clear")) {
1✔
258
                final T newClass = new ClassInstance<T>().newInstance(this.clazz);
1✔
259
                final T expectedClass = new ClassInstance<T>().newInstance(this.clazz);
1✔
260
                try {
261
                    // Perform any Post Construction on object without parameters
262
                    List<Annotation> annotations = null;
1✔
263
                    for (final Method mt : methods) {
1✔
264
                        annotations = Arrays.asList(mt.getAnnotations());
1✔
265
                        for (final Annotation annotation : annotations) {
1✔
266
                            // XXX On purpose logic change to support both javax and jakarta namespace for annotations
267
                            if ("PostConstruct".equals(annotation.annotationType().getSimpleName())
1!
268
                                    && mt.getParameterTypes().length == 0) {
1!
269
                                // Invoke method newClass
270
                                mt.invoke(newClass);
1✔
271
                                // Invoke method expectedClass
272
                                mt.invoke(expectedClass);
1✔
273
                            }
274
                        }
1✔
275
                    }
276
                    // Invoke clear only on newClass
277
                    newClass.getClass().getMethod("clear").invoke(newClass);
1✔
278
                    Assertions.assertEquals(expectedClass, newClass,
1✔
279
                            String.format("Clear method does not match new object '%s'", this.clazz));
1✔
UNCOV
280
                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
×
281
                        | NoSuchMethodException | SecurityException e) {
UNCOV
282
                    Assertions.fail(String.format("An exception was thrown while testing the Clear method '%s' : '%s'",
×
UNCOV
283
                            this.clazz.getName(), e.toString()));
×
284
                }
1✔
285
            }
286
        }
287
    }
1✔
288

289
    /**
290
     * Constructors test.
291
     */
292
    void constructorsTest() {
293
        for (final Constructor<?> constructor : this.clazz.getConstructors()) {
1✔
294

295
            // Skip deprecated constructors
296
            if (constructor.isAnnotationPresent(Deprecated.class)) {
1✔
297
                continue;
1✔
298
            }
299

300
            final Class<?>[] types = constructor.getParameterTypes();
1✔
301

302
            final Object[] values = new Object[constructor.getParameterTypes().length];
1✔
303

304
            // Load Data
305
            for (int i = 0; i < values.length; i++) {
1✔
306
                values[i] = this.buildValue(types[i], LoadType.STANDARD_DATA);
1✔
307
            }
308

309
            try {
310
                constructor.newInstance(values);
1✔
UNCOV
311
            } catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) {
×
UNCOV
312
                Assertions.fail(
×
UNCOV
313
                        String.format("An exception was thrown while testing the constructor(s) '%s' with '%s': '%s'",
×
UNCOV
314
                                constructor.getName(), Arrays.toString(values), e.toString()));
×
315
            }
1✔
316

317
            // TODO 1/12/2019 JWL Add checking of new object properties
318
        }
319
    }
1✔
320

321
    /**
322
     * Check Serializable test.
323
     */
324
    void checkSerializableTest() {
325
        final T object = new ClassInstance<T>().newInstance(this.clazz);
1✔
326
        if (this.implementsSerializable(object)) {
1✔
327
            final T newObject = this.canSerialize(object);
1✔
328
            // Toggle to throw or not throw error with only one way working
329
            if (this.skipStrictSerializable != SkipStrictSerialize.ON) {
1✔
330
                Assertions.assertEquals(object, newObject);
1✔
331
            } else {
332
                Assertions.assertNotEquals(object, newObject);
1✔
333
            }
334
            return;
1✔
335
        }
336

337
        // Only throw error when specifically checking on serialization
338
        if (this.checkSerializable == CheckSerialize.ON) {
1!
UNCOV
339
            Assertions.fail(String.format("Class is not serializable '%s'", object.getClass().getName()));
×
340
        }
341
    }
1✔
342

343
    /**
344
     * Implements serializable.
345
     *
346
     * @param object
347
     *            the object
348
     *
349
     * @return true, if successful
350
     */
351
    boolean implementsSerializable(final T object) {
352
        return object instanceof Externalizable || object instanceof Serializable;
1!
353
    }
354

355
    /**
356
     * Can serialize.
357
     *
358
     * @param object
359
     *            the object
360
     *
361
     * @return object read after serialization
362
     */
363
    @SuppressWarnings("unchecked")
364
    T canSerialize(final T object) {
365
        byte[] byteArray = null;
1✔
366

367
        // Serialize data
368
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
1✔
369
                ObjectOutputStream oos = new ObjectOutputStream(baos)) {
1✔
370
            oos.writeObject(object);
1✔
371
            byteArray = baos.toByteArray();
1✔
UNCOV
372
        } catch (final IOException e) {
×
UNCOV
373
            Assertions.fail(String.format("An exception was thrown while serializing the class '%s': '%s',",
×
UNCOV
374
                    object.getClass().getName(), e.toString()));
×
UNCOV
375
            return null;
×
376
        }
1✔
377

378
        // Deserialize Data
379
        try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
1✔
380
                ObjectInputStream ois = new ObjectInputStream(bais)) {
1✔
381
            return (T) ois.readObject();
1✔
382
        } catch (final ClassNotFoundException | IOException e) {
×
383
            Assertions.fail(String.format("An exception was thrown while deserializing the class '%s': '%s',",
×
UNCOV
384
                    object.getClass().getName(), e.toString()));
×
385
        }
UNCOV
386
        return null;
×
387
    }
388

389
    /**
390
     * Builds the value.
391
     *
392
     * @param <R>
393
     *            the generic type
394
     * @param returnType
395
     *            the return type
396
     * @param loadType
397
     *            the load type
398
     *
399
     * @return the object
400
     */
401
    private <R> Object buildValue(final Class<R> returnType, final LoadType loadType) {
402
        final ValueBuilder valueBuilder = new ValueBuilder();
1✔
403
        valueBuilder.setLoadData(this.loadData);
1✔
404
        return valueBuilder.buildValue(returnType, loadType);
1✔
405
    }
406

407
    /**
408
     * Tests the equals/hashCode/toString methods of the specified class.
409
     */
410
    public void equalsHashCodeToStringSymmetricTest() {
411
        // Run Equals Verifier
412
        this.processEqualsVerifierSymmetricTest();
1✔
413

414
        // Create two new instances of the class under test and test equals, hashcode, and toString on them
415
        final T x = new ClassInstance<T>().newInstance(this.clazz);
1✔
416
        final T y = new ClassInstance<T>().newInstance(this.clazz);
1✔
417
        this.processClassEqualsHashCodeToStringSymmetricTest(x, y);
1✔
418

419
        // TODO 1/12/2019 JWL Internalize extension will require canEquals, equals, hashcode, and toString overrides.
420
        /*
421
         * try { this.extension = (Class<E>) new ExtensionBuilder<T>().generate(this.clazz); } catch (NotFoundException
422
         * e) { Assert.fail(e.getMessage()); } catch (CannotCompileException e) { Assert.fail(e.getMessage()); }
423
         */
424

425
        // If there is an extension, create an instance and test equals, hashcode, and toString on it against the empty
426
        // class instance
427
        if (this.extension != null) {
1✔
428
            final E ext = new ClassInstance<E>().newInstance(this.extension);
1✔
429
            this.processExtensionEqualsHashCodeToStringSymmetricTest(y, ext);
1✔
430
        }
431
    }
1✔
432

433
    /**
434
     * Process equals verifier symmetric test.
435
     */
436
    private void processEqualsVerifierSymmetricTest() {
437
        // Run Equals Verifier
438
        try {
439
            EqualsVerifier.simple().forClass(this.clazz).suppress(Warning.BIGDECIMAL_EQUALITY).verify();
1✔
UNCOV
440
        } catch (AssertionError e) {
×
UNCOV
441
            JavaBeanTesterWorker.logger.warn("EqualsVerifier attempt failed: {}", e.getMessage());
×
442
        }
1✔
443
    }
1✔
444

445
    /**
446
     * Process class equals hash code to string symmetric test.
447
     *
448
     * @param x
449
     *            the x
450
     * @param y
451
     *            the y
452
     */
453
    private void processClassEqualsHashCodeToStringSymmetricTest(final T x, final T y) {
454
        // Verify that the objects were created successfully before testing equals, hashcode, and toString
455
        Assertions.assertNotNull(x,
1✔
456
                String.format("Create new instance of class '%s' resulted in null", this.clazz.getName()));
1✔
457
        Assertions.assertNotNull(y,
1✔
458
                String.format("Create new instance of class '%s' resulted in null", this.clazz.getName()));
1✔
459

460
        // Test Equals, HashCode, and ToString on Empty Objects
461
        Assertions.assertEquals(x, y,
1✔
462
                String.format(".equals() should be consistent for two empty objects of type %s", this.clazz.getName()));
1✔
463
        Assertions.assertEquals(x.hashCode(), y.hashCode(), String
1✔
464
                .format(".hashCode() should be consistent for two empty objects of type %s", this.clazz.getName()));
1✔
465
        Assertions.assertEquals(x.toString(), y.toString(), String
1✔
466
                .format(".toString() should be consistent for two empty objects of type %s", this.clazz.getName()));
1✔
467

468
        // Test One Sided Tests on Empty Objects
469
        Assertions.assertNotEquals(x, null,
1✔
470
                String.format("An empty object of type %s should not be equal to null", this.clazz.getName()));
1✔
471
        Assertions.assertEquals(x, x,
1✔
472
                String.format("An empty object of type %s should be equal to itself", this.clazz.getName()));
1✔
473

474
        // If the class has setters, the previous tests would have been against empty classes
475
        // If so, load the classes and re-test
476
        if (this.classHasSetters(this.clazz)) {
1✔
477
            // Populate Side X
478
            JavaBeanTesterWorker.load(this.clazz, x, this.loadData);
1✔
479

480
            // ReTest Equals (flip)
481
            Assertions.assertNotEquals(y, x,
1✔
482
                    String.format(".equals() should not be consistent for one empty and one loaded object of type %s",
1✔
483
                            this.clazz.getName()));
1✔
484

485
            // Populate Side Y
486
            JavaBeanTesterWorker.load(this.clazz, y, this.loadData);
1✔
487

488
            // ReTest Equals and HashCode
489
            if (this.loadData == LoadData.ON) {
1✔
490
                Assertions.assertEquals(x, y,
1✔
491
                        String.format(".equals() should be equal for two instances of type %s with loaded data",
1✔
492
                                this.clazz.getName()));
1✔
493
                Assertions.assertEquals(x.hashCode(), y.hashCode(),
1✔
494
                        String.format(".hashCode() should be equal for two instances of type %s with loaded data",
1✔
495
                                this.clazz.getName()));
1✔
496
            } else {
497
                // Part of the data loaded may be the same, so we cannot guarantee that equals and hashcode will be
498
                // different, but we can at least check that they are not both the same
499
                // Assertions.assertNotEquals(x, y);
500
                // Assertions.assertNotEquals(x.hashCode(), y.hashCode());
501
                if (x.hashCode() == y.hashCode()) {
1✔
502
                    Assertions.assertEquals(x, y);
1✔
503
                    Assertions.assertEquals(x.hashCode(), y.hashCode());
1✔
504
                } else {
505
                    Assertions.assertNotEquals(x, y);
1✔
506
                    Assertions.assertNotEquals(x.hashCode(), y.hashCode());
1✔
507
                }
508
            }
509
        }
510

511
        // Create Immutable Instance
512
        try {
513
            final T e = new ClassInstance<T>().newInstance(this.clazz);
1✔
514
            ByteBuddyBeanCopier.copy(x, e, (value, targetType) -> {
1✔
515
                if (targetType == boolean.class) {
1✔
516
                    return value == null ? Boolean.FALSE : value;
1!
517
                }
518
                return value;
1✔
519
            });
520
            Assertions.assertEquals(e, x);
1✔
UNCOV
521
        } catch (final Exception e) {
×
UNCOV
522
            JavaBeanTesterWorker.logger.trace("Do nothing class is not mutable", e);
×
523
        }
1✔
524
    }
1✔
525

526
    /**
527
     * Process extension equals hash code to string symmetric test.
528
     *
529
     * @param y
530
     *            the y
531
     * @param ext
532
     *            the ext
533
     */
534
    private void processExtensionEqualsHashCodeToStringSymmetricTest(final T y, final E ext) {
535
        // Verify that the objects were created successfully before testing equals, hashcode, and toString
536
        Assertions.assertNotNull(ext,
1✔
537
                String.format("Create new instance of extension %s resulted in null", this.extension.getName()));
1✔
538

539
        // Test Extension Equals, HashCode, and ToString on Empty Objects
540
        Assertions.assertNotEquals(ext, y,
1✔
541
                String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
1✔
542
                        this.extension.getName(), this.clazz.getName()));
1✔
543
        Assertions.assertNotEquals(ext.hashCode(), y.hashCode(),
1✔
544
                String.format(".hashCode() should not be equal for extension of type %s and empty object of type %s",
1✔
545
                        this.extension.getName(), this.clazz.getName()));
1✔
546
        Assertions.assertNotEquals(ext.toString(), y.toString(),
1✔
547
                String.format(".toString() should not be equal for extension of type %s and empty object of type %s",
1✔
548
                        this.extension.getName(), this.clazz.getName()));
1✔
549

550
        // Test Extension One Sided Tests on Empty Objects
551
        Assertions.assertNotEquals(ext, null,
1✔
552
                String.format("An empty extension of type %s should not be equal to null", this.clazz.getName()));
1✔
553
        Assertions.assertEquals(ext, ext,
1✔
554
                String.format("An empty extension of type %s should be equal to itself", this.extension.getName()));
1✔
555

556
        // If the class has setters, the previous tests would have been against empty classes
557
        // If so, load the classes and re-test
558
        if (this.classHasSetters(this.clazz)) {
1✔
559
            // Populate Extension Side Ext
560
            JavaBeanTesterWorker.load(this.extension, ext, this.loadData);
1✔
561

562
            // ReTest Extension Equals (flip)
563
            Assertions.assertNotEquals(y, ext,
1✔
564
                    String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
1✔
565
                            this.extension.getName(), this.clazz.getName()));
1✔
566

567
            // Populate Side Y
568
            JavaBeanTesterWorker.load(this.clazz, y, this.loadData);
1✔
569

570
            // ReTest Extension Equals, HashCode, and ToString
571
            Assertions.assertNotEquals(ext, y,
1✔
572
                    String.format(".equals() should not be equal for extension of type %s and empty object of type %s",
1✔
573
                            this.extension.getName(), this.clazz.getName()));
1✔
574
            Assertions.assertNotEquals(ext.hashCode(), y.hashCode(),
1✔
575
                    String.format(
1✔
576
                            ".hashCode() should not be equal for extension of type %s and empty object of type %s",
577
                            this.extension.getName(), this.clazz.getName()));
1✔
578
            Assertions.assertNotEquals(ext.toString(), y.toString(),
1✔
579
                    String.format(
1✔
580
                            ".toString() should not be equal for extension of type %s and empty object of type %s",
581
                            this.extension.getName(), this.clazz.getName()));
1✔
582
        }
583

584
        // If class is final, use Object.class for comparison needs
585
        if (Modifier.isFinal(clazz.getModifiers())) {
1✔
586
            JavaBeanTesterWorker.logger.trace("Final object does not go through final equals check");
1✔
587
            return;
1✔
588
        }
589

590
        // Create Extension Immutable Instance
591
        try {
592
            final E e = new ClassInstance<E>().newInstance(this.extension);
1✔
593
            ByteBuddyBeanCopier.copy(ext, e, (value, targetType) -> {
1✔
594
                if (targetType == boolean.class) {
1✔
595
                    return value == null ? Boolean.FALSE : value;
1!
596
                }
597
                return value;
1✔
598
            });
599
            Assertions.assertEquals(e, ext);
1✔
UNCOV
600
        } catch (final Exception e) {
×
601
            JavaBeanTesterWorker.logger.trace("Do nothing class is not mutable", e);
×
602
        }
1✔
603
    }
1✔
604

605
    /**
606
     * Equals Tests will traverse one object changing values until all have been tested against another object. This is
607
     * done to effectively test all paths through equals.
608
     *
609
     * @param instance
610
     *            the class instance under test.
611
     * @param expected
612
     *            the instance expected for tests.
613
     */
614
    void equalsTests(final T instance, final T expected) {
615

616
        // Perform hashCode test dependent on data coming in
617
        // Assert.assertEquals(expected.hashCode(), instance.hashCode());
618
        if (expected.hashCode() == instance.hashCode()) {
1✔
619
            Assertions.assertEquals(expected.hashCode(), instance.hashCode());
1✔
620
        } else {
621
            Assertions.assertNotEquals(expected.hashCode(), instance.hashCode());
1✔
622
        }
623

624
        final ValueBuilder valueBuilder = new ValueBuilder();
1✔
625
        valueBuilder.setLoadData(this.loadData);
1✔
626

627
        final PropertyDescriptor[] props = this.getProps(instance.getClass());
1✔
628
        for (final PropertyDescriptor prop : props) {
1✔
629
            Method getter = prop.getReadMethod();
1✔
630
            final Method setter = prop.getWriteMethod();
1✔
631

632
            // Java Metro Bug Patch (Boolean Wrapper usage of 'is' possible
633
            if (getter == null && setter != null) {
1!
634
                final String isBooleanWrapper = "is" + setter.getName().substring(3);
1✔
635
                try {
636
                    getter = this.clazz.getMethod(isBooleanWrapper);
1✔
UNCOV
637
                } catch (NoSuchMethodException | SecurityException e) {
×
638
                    // Do nothing
639
                }
1✔
640
            }
641

642
            if (getter != null && setter != null) {
1!
643
                // We have both a get and set method for this property
644
                final Class<?> returnType = getter.getReturnType();
1✔
645
                final Class<?>[] params = setter.getParameterTypes();
1✔
646

647
                if (params.length == 1 && params[0] == returnType) {
1!
648
                    // The set method has 1 argument, which is of the same type as the return type of the get method, so
649
                    // we can test this property
650
                    try {
651
                        // Save original value
652
                        final Object original = getter.invoke(instance);
1✔
653

654
                        // Build a value of the correct type to be passed to the set method using alternate test
655
                        Object value = valueBuilder.buildValue(returnType, LoadType.ALTERNATE_DATA);
1✔
656

657
                        // Call the set method, then check the same value comes back out of the get method
658
                        setter.invoke(instance, value);
1✔
659

660
                        // Check equals depending on data
661
                        if (instance.equals(expected)) {
1✔
662
                            Assertions.assertEquals(expected, instance);
1✔
663
                        } else {
664
                            Assertions.assertNotEquals(expected, instance);
1✔
665
                        }
666

667
                        // Build a value of the correct type to be passed to the set method using null test
668
                        value = valueBuilder.buildValue(returnType, LoadType.NULL_DATA);
1✔
669

670
                        // Call the set method, then check the same value comes back out of the get method
671
                        setter.invoke(instance, value);
1✔
672

673
                        // Check equals depending on data
674
                        if (instance.equals(expected)) {
1✔
675
                            Assertions.assertEquals(expected, instance);
1✔
676
                        } else {
677
                            Assertions.assertNotEquals(expected, instance);
1✔
678
                        }
679

680
                        // Reset to original value
681
                        setter.invoke(instance, original);
1✔
682

UNCOV
683
                    } catch (final IllegalAccessException | IllegalArgumentException | InvocationTargetException
×
684
                            | SecurityException e) {
UNCOV
685
                        Assertions.fail(
×
UNCOV
686
                                String.format("An exception was thrown while testing the property (equals) '%s': '%s'",
×
UNCOV
687
                                        prop.getName(), e.toString()));
×
688
                    }
1✔
689
                }
690
            }
691
        }
692
    }
1✔
693

694
    /**
695
     * Class has setters.
696
     *
697
     * @param clazz
698
     *            the clazz
699
     *
700
     * @return true, if successful
701
     */
702
    private boolean classHasSetters(final Class<T> clazz) {
703
        return Arrays.stream(this.getProps(clazz))
1✔
704
                .anyMatch(propertyDescriptor -> propertyDescriptor.getWriteMethod() != null);
1✔
705
    }
706

707
    /**
708
     * Gets the props.
709
     *
710
     * @param clazz
711
     *            the clazz
712
     *
713
     * @return the props
714
     */
715
    private PropertyDescriptor[] getProps(final Class<?> clazz) {
716
        try {
717
            final List<PropertyDescriptor> usedProps = new ArrayList<>(
1✔
718
                    Introspector.getBeanInfo(clazz).getPropertyDescriptors().length);
1✔
719
            final List<PropertyDescriptor> props = Arrays
1✔
720
                    .asList(Introspector.getBeanInfo(clazz).getPropertyDescriptors());
1✔
721
            for (final PropertyDescriptor prop : props) {
1✔
722
                // Check the list of properties that we don't want to test
723
                if (this.skipThese.contains(prop.getName())) {
1✔
724
                    continue;
1✔
725
                }
726
                usedProps.add(prop);
1✔
727
            }
1✔
728
            return usedProps.toArray(new PropertyDescriptor[0]);
1✔
UNCOV
729
        } catch (final IntrospectionException e) {
×
UNCOV
730
            Assertions.fail(String.format("An exception was thrown while testing class '%s': '%s'",
×
UNCOV
731
                    this.clazz.getName(), e.toString()));
×
UNCOV
732
            return new PropertyDescriptor[0];
×
733
        }
734
    }
735

736
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc