View Javadoc
1   /*
2    * SPDX-License-Identifier: Apache-2.0
3    * See LICENSE file for details.
4    *
5    * Copyright 2012-2026 hazendaz
6    *
7    * Portions of initial baseline code (getter/setter test) by Rob Dawson (CodeBox)
8    */
9   package com.codebox.bean;
10  
11  import java.io.ByteArrayInputStream;
12  import java.io.ByteArrayOutputStream;
13  import java.io.IOException;
14  import java.io.NotSerializableException;
15  import java.io.ObjectInputStream;
16  import java.io.ObjectOutputStream;
17  import java.util.Date;
18  import java.util.Objects;
19  
20  import net.bytebuddy.description.type.TypeDescription;
21  
22  import org.junit.jupiter.api.Assertions;
23  import org.junit.jupiter.api.BeforeEach;
24  import org.junit.jupiter.api.Test;
25  import org.opentest4j.AssertionFailedError;
26  import org.powermock.reflect.Whitebox;
27  
28  /**
29   * The Class JavaBeanTesterTest.
30   */
31  class JavaBeanTesterTest {
32  
33      /** The sample bean. */
34      private SampleBean sampleBean;
35  
36      /** The expected bean. */
37      private SampleBean expectedBean;
38  
39      /**
40       * Inits the.
41       */
42      @BeforeEach
43      void init() {
44          this.sampleBean = new SampleBean(null);
45          this.expectedBean = new SampleBean(null);
46      }
47  
48      /**
49       * Load full bean.
50       */
51      @Test
52      void loadFullBean() {
53          JavaBeanTester.builder(SampleBean.class).loadData().testInstance(this.sampleBean);
54          Assertions.assertNotNull(this.sampleBean.getDoubleWrapper());
55      }
56  
57      /**
58       * Load full bean equals.
59       */
60      @Test
61      void loadFullBeanEquals() {
62          JavaBeanTester.builder(SampleBean.class).loadData().testInstance(this.sampleBean);
63          JavaBeanTester.builder(SampleBean.class).loadData().testInstance(this.expectedBean);
64  
65          this.sampleBean.setSampleDepthBean(new SampleDepthBean());
66          this.expectedBean.setSampleDepthBean(new SampleDepthBean());
67          this.sampleBean.setEmptyBean(new EmptyBean());
68          this.expectedBean.setEmptyBean(new EmptyBean());
69          JavaBeanTester.builder(SampleDepthBean.class).loadData().testInstance(this.sampleBean.getSampleDepthBean());
70          JavaBeanTester.builder(SampleDepthBean.class).loadData().testInstance(this.expectedBean.getSampleDepthBean());
71  
72          JavaBeanTester.builder(SampleBean.class).loadData().testEquals(this.sampleBean, this.expectedBean);
73      }
74  
75      /**
76       * Load full bean equals short.
77       */
78      @Test
79      void loadFullBeanEqualsShort() {
80          JavaBeanTester.builder(SampleBean.class).loadData().testInstance(this.sampleBean);
81          JavaBeanTester.builder(SampleBean.class).loadData().testInstance(this.expectedBean);
82          JavaBeanTester.builder(SampleBean.class).loadData().testEquals(this.sampleBean, this.expectedBean);
83      }
84  
85      /**
86       * Load full bean equals skip underlying.
87       */
88      @Test
89      void loadFullBeanEqualsSkipUnderlying() {
90          JavaBeanTester.builder(SampleBean.class).testInstance(this.sampleBean);
91          JavaBeanTester.builder(SampleBean.class).testInstance(this.expectedBean);
92          JavaBeanTester.builder(SampleBean.class).testEquals(this.sampleBean, this.expectedBean);
93      }
94  
95      /**
96       * Load_full bean skip underlying data.
97       */
98      @Test
99      void loadFullBeanSkipUnderlyingData() {
100         JavaBeanTester.builder(SampleBean.class).testInstance(this.sampleBean);
101         Assertions.assertNotNull(this.sampleBean.getDoubleWrapper());
102     }
103 
104     /**
105      * Load partial bean equals.
106      */
107     @Test
108     void loadPartialBeanEquals() {
109         JavaBeanTester.builder(SampleBean.class).loadData().testInstance(this.sampleBean);
110         JavaBeanTester.builder(SampleBean.class).loadData().testInstance(this.expectedBean);
111 
112         this.sampleBean.setSampleDepthBean(new SampleDepthBean());
113         this.expectedBean.setSampleDepthBean(new SampleDepthBean());
114         JavaBeanTester.builder(SampleDepthBean.class).loadData().testInstance(this.sampleBean.getSampleDepthBean());
115         JavaBeanTester.builder(SampleDepthBean.class).loadData().testInstance(this.expectedBean.getSampleDepthBean());
116 
117         JavaBeanTester.builder(SampleBean.class).loadData().testEquals(this.sampleBean, this.expectedBean);
118     }
119 
120     /**
121      * Load skip bean properties.
122      */
123     @Test
124     void loadSkipBeanProperties() {
125         JavaBeanTester.builder(SampleBean.class).loadData().skip("string").testInstance(this.sampleBean);
126         Assertions.assertNotNull(this.sampleBean.getDoubleWrapper());
127         Assertions.assertNull(this.sampleBean.getString());
128     }
129 
130     /**
131      * Test full bean.
132      */
133     @Test
134     void testFullBean() {
135         JavaBeanTester.builder(SampleBean.class).checkEquals().loadData().test();
136     }
137 
138     /**
139      * Test full bean.
140      */
141     @Test
142     void testFullBeanWithExtension() {
143         JavaBeanTester.builder(SampleBean.class, SampleExtensionBean.class).checkEquals().loadData().test();
144     }
145 
146     /**
147      * Test full bean null ext.
148      */
149     @Test
150     void testFullBeanNullExt() {
151         JavaBeanTester.builder(SampleBean.class).checkEquals().loadData().test();
152         JavaBeanTester.builder(SampleValueObject.class).checkEquals().loadData().test();
153     }
154 
155     /**
156      * Test full bean skip underlying data.
157      */
158     @Test
159     void testFullBeanSkipUnderlyingData() {
160         JavaBeanTester.builder(SampleBean.class).checkEquals().test();
161     }
162 
163     /**
164      * Test full bean skip underlying data.
165      */
166     @Test
167     void testFullBeanSkipUnderlyingDataWithExtension() {
168         JavaBeanTester.builder(SampleBean.class, SampleExtensionBean.class).checkEquals().test();
169     }
170 
171     /**
172      * Test skip bean properties.
173      */
174     @Test
175     void testSkipBeanProperties() {
176         JavaBeanTester.builder(SampleBean.class).checkEquals().loadData().skip("string").test();
177     }
178 
179     /**
180      * Test skip bean properties.
181      */
182     @Test
183     void testSkipBeanPropertiesWithExtension() {
184         JavaBeanTester.builder(SampleBean.class, SampleExtensionBean.class).checkEquals().loadData().skip("string")
185                 .test();
186     }
187 
188     /**
189      * Test skip bean properties null just ignores the skipping.
190      */
191     @Test
192     void testSkipBeanPropertiesNull() {
193         JavaBeanTester.builder(SampleBean.class).checkEquals().loadData().skip((String[]) null).test();
194     }
195 
196     /**
197      * Test skip bean properties null just ignores the skipping.
198      */
199     @Test
200     void testSkipBeanPropertiesNullWithExtension() {
201         JavaBeanTester.builder(SampleBean.class, SampleExtensionBean.class).checkEquals().loadData()
202                 .skip((String[]) null).test();
203     }
204 
205     /**
206      * Test skip can equals.
207      */
208     @Test
209     void testSkipCanEquals() {
210         JavaBeanTester.builder(SampleBean.class).loadData().test();
211     }
212 
213     /**
214      * Test skip can equals.
215      */
216     @Test
217     void testSkipCanEqualsWithExtension() {
218         JavaBeanTester.builder(SampleBean.class, SampleExtensionBean.class).loadData().test();
219     }
220 
221     /**
222      * TestSkip all as false.
223      */
224     @Test
225     void testSkipCanEqualsFalse() {
226         JavaBeanTester.builder(SampleBean.class).checkEquals(false).checkSerializable(false).loadData(false).test();
227     }
228 
229     /**
230      * Test skip all as false.
231      */
232     @Test
233     void testSkipCanEqualsFalseWithExtension() {
234         JavaBeanTester.builder(SampleBean.class, SampleExtensionBean.class).checkEquals(false).checkSerializable(false)
235                 .loadData(false).test();
236     }
237 
238     /**
239      * Test serializable.
240      */
241     @Test
242     void testSerializable() {
243         JavaBeanTester.builder(SerializableBean.class).checkSerializable().test();
244     }
245 
246     /**
247      * Test non serializable.
248      */
249     @Test
250     void testNonSerializableInternallyFails() {
251         JavaBeanTester.builder(NonSerializableBean.class).checkSerializable().skipStrictSerializable().test();
252     }
253 
254     /**
255      * Test non deserializable.
256      *
257      * @throws Exception
258      *             generic exception.
259      */
260     @Test
261     void testNonSerializable() throws Exception {
262         final NonDeserializableBean bean = new NonDeserializableBean();
263         bean.getList().add(new Object());
264 
265         Assertions.assertThrows(NotSerializableException.class, () -> {
266             JavaBeanTesterTest.serialize(bean);
267         });
268     }
269 
270     /**
271      * Test clear.
272      */
273     @Test
274     void testClear() {
275         JavaBeanTester.builder(SerializableBean.class).checkClear().test();
276     }
277 
278     /**
279      * Test clear false.
280      */
281     @Test
282     void testClearFalse() {
283         JavaBeanTester.builder(SerializableBean.class).checkClear(false).test();
284     }
285 
286     /**
287      * Test constructor.
288      */
289     @Test
290     void testConstructor() {
291         JavaBeanTester.builder(SerializableBean.class).checkConstructor().test();
292     }
293 
294     /**
295      * Test constructor false.
296      */
297     @Test
298     void testConstructorFalse() {
299         JavaBeanTester.builder(SerializableBean.class).checkConstructor(false).test();
300     }
301 
302     /**
303      * Test multi constructor.
304      */
305     @Test
306     void testMultiConstructor() {
307         JavaBeanTester.builder(MultiConstructorBean.class).test();
308     }
309 
310     /**
311      * Test multi constructor null extension.
312      */
313     @Test
314     void testMultiConstructorNullExtension() {
315         JavaBeanTester.builder(MultiConstructorBean.class, null).checkEquals().test();
316     }
317 
318     /**
319      * Test temporary single mode.
320      */
321     // TODO 1/12/2019 JWL Temporary until we start using internalized extension logic
322     @Test
323     void testTemporarySingleMode() {
324         final JavaBeanTesterBuilder<String, Object> builder = new JavaBeanTesterBuilder<>(String.class);
325         final JavaBeanTesterWorker<String, Object> worker = Whitebox.getInternalState(builder, "worker");
326         Assertions.assertEquals(String.class, worker.getClazz());
327     }
328 
329     /**
330      * Test sample lombok builder.
331      */
332     @Test
333     void testSampleLombokBuilder() {
334         JavaBeanTester.builder(SampleLombokBuilder.class).checkEquals().checkConstructor().checkClear().loadData()
335                 .test();
336     }
337 
338     /**
339      * Test sample lombok builder null extension.
340      */
341     @Test
342     void testSampleLombokBuilderNullExtension() {
343         JavaBeanTester.builder(SampleLombokBuilder.class, null).checkEquals().checkConstructor().checkClear().loadData()
344                 .test();
345     }
346 
347     @Test
348     void testCanEqualInterceptor() {
349         final JavaBeanTester.CanEqualInterceptor interceptor = new JavaBeanTester.CanEqualInterceptor();
350         Assertions.assertNotNull(interceptor);
351         Assertions.assertTrue(
352                 JavaBeanTester.CanEqualInterceptor.canEqual(new TypeDescription.ForLoadedType(SampleBean.class)));
353         Assertions.assertFalse(JavaBeanTester.CanEqualInterceptor.canEqual(new Object()));
354     }
355 
356     @Test
357     void testInstanceOfEqualsInterceptor() {
358         final JavaBeanTester.InstanceOfEqualsInterceptor interceptor = new JavaBeanTester.InstanceOfEqualsInterceptor();
359         Assertions.assertNotNull(interceptor);
360 
361         final HashValue sameLeft = new HashValue(10);
362         final HashValue sameRight = new HashValue(10);
363         final HashValue different = new HashValue(11);
364 
365         Assertions.assertTrue(JavaBeanTester.InstanceOfEqualsInterceptor.equals(sameLeft, sameLeft));
366         Assertions.assertFalse(JavaBeanTester.InstanceOfEqualsInterceptor.equals(sameLeft, null));
367         Assertions.assertFalse(JavaBeanTester.InstanceOfEqualsInterceptor.equals(sameLeft, "other-type"));
368         Assertions.assertTrue(JavaBeanTester.InstanceOfEqualsInterceptor.equals(sameLeft, sameRight));
369         Assertions.assertFalse(JavaBeanTester.InstanceOfEqualsInterceptor.equals(sameLeft, different));
370     }
371 
372     /**
373      * Test that checkSerializable() on a non-Serializable class calls Assertions.fail (covers L338-true + L339 in
374      * JavaBeanTesterWorker).
375      */
376     @Test
377     void testCheckSerializableOnNonSerializableClassFails() {
378         Assertions.assertThrows(AssertionFailedError.class,
379                 () -> JavaBeanTester.builder(SimpleNonSerializableBean.class, null).checkSerializable().test());
380     }
381 
382     /**
383      * Test that EqualsVerifier failure is swallowed (covers L440-441 in JavaBeanTesterWorker).
384      */
385     @Test
386     void testCheckEqualsSwallowsEqualsVerifierFailure() {
387         // IdentityEqualsStringBean uses == for String comparison; EqualsVerifier creates non-interned strings
388         // so it fails – but the error is caught and logged, not propagated.
389         // Pass null extension to skip processExtensionEqualsHashCodeToStringSymmetricTest entirely.
390         // The worker's own checks still pass because ValueBuilder always provides the same interned "TEST_VALUE".
391         JavaBeanTester.builder(IdentityEqualsStringBean.class, null).checkEquals().loadData().test();
392     }
393 
394     /**
395      * Test that an Externalizable class covers the Externalizable branch in implementsSerializable (L352) and the
396      * skipStrictSerializable else-branch assertNotEquals (L339).
397      */
398     @Test
399     void testExternalizableBean() {
400         // SampleExternalizableBean uses identity equals; after round-trip the two instances differ →
401         // skipStrictSerializable triggers assertNotEquals (L339); Externalizable branch in
402         // implementsSerializable (L352) is also covered.
403         JavaBeanTester.builder(SampleExternalizableBean.class).checkSerializable().skipStrictSerializable().test();
404     }
405 
406     /**
407      * Test that a Serializable class with a non-serializable field triggers IOException in canSerialize (L372-375).
408      */
409     @Test
410     void testSerializableWithNonSerializableFieldFails() {
411         Assertions.assertThrows(AssertionFailedError.class,
412                 () -> JavaBeanTester.builder(SerializableWithThreadFieldBean.class).checkSerializable().test());
413     }
414 
415     /**
416      * Test that Date.class exercises the date-normalization guard (L231+).
417      */
418     @Test
419     void testDateBeanGetterSetter() {
420         // Pass null extension to bypass ByteBuddy (forbidden to subclass java.util.Date)
421         JavaBeanTester.builder(Date.class, null).loadData().test();
422     }
423 
424     @Test
425     void testGetterSetterFailureIsReported() {
426         Assertions.assertThrows(AssertionFailedError.class,
427                 () -> JavaBeanTester.builder(GetterSetterThrowingBean.class, null).loadData().checkEquals(false)
428                         .checkConstructor(false).checkClear(false).checkSerializable(false).test());
429     }
430 
431     @Test
432     void testClearFailureIsReported() {
433         Assertions.assertThrows(AssertionFailedError.class, () -> JavaBeanTester.builder(ClearThrowingBean.class, null)
434                 .checkClear().checkEquals(false).checkConstructor(false).checkSerializable(false).test());
435     }
436 
437     @Test
438     void testConstructorFailureIsReported() {
439         Assertions.assertThrows(AssertionFailedError.class,
440                 () -> JavaBeanTester.builder(ConstructorThrowingBean.class, null).checkConstructor().checkEquals(false)
441                         .checkClear(false).checkSerializable(false).test());
442     }
443 
444     @Test
445     void testCheckSerializableFailureBranch() {
446         Assertions.assertThrows(AssertionFailedError.class, () -> JavaBeanTester.builder(Object.class, null)
447                 .checkSerializable().checkEquals(false).checkConstructor(false).checkClear(false).test());
448     }
449 
450     @Test
451     void testDeserializeFailureIsReported() {
452         Assertions.assertThrows(AssertionFailedError.class,
453                 () -> JavaBeanTester.builder(SerializableWithFailingReadObject.class, null).checkSerializable()
454                         .checkEquals(false).checkConstructor(false).checkClear(false).test());
455     }
456 
457     @Test
458     void testProcessClassCopyFailureIsSwallowed() throws Exception {
459         CopyFailureAfterLoadBean.resetSetCalls();
460         final JavaBeanTesterWorker<CopyFailureAfterLoadBean, Object> worker = new JavaBeanTesterWorker<>(
461                 CopyFailureAfterLoadBean.class);
462         worker.setLoadData(com.codebox.enums.LoadData.ON);
463         Whitebox.invokeMethod(worker, "processClassEqualsHashCodeToStringSymmetricTest", new CopyFailureAfterLoadBean(),
464                 new CopyFailureAfterLoadBean());
465     }
466 
467     @Test
468     void testProcessExtensionCopyFailureIsSwallowed() throws Exception {
469         final JavaBeanTesterWorker<NoSetterBaseBean, ExtensionCopyFailureBean> worker = new JavaBeanTesterWorker<>(
470                 NoSetterBaseBean.class, ExtensionCopyFailureBean.class);
471         worker.setLoadData(com.codebox.enums.LoadData.ON);
472         Whitebox.invokeMethod(worker, "processExtensionEqualsHashCodeToStringSymmetricTest", new NoSetterBaseBean(),
473                 new ExtensionCopyFailureBean());
474     }
475 
476     @Test
477     void testEqualsSetterFailureIsReported() {
478         final JavaBeanTesterWorker<EqualsSetterThrowingBean, Object> worker = new JavaBeanTesterWorker<>(
479                 EqualsSetterThrowingBean.class);
480         worker.setLoadData(com.codebox.enums.LoadData.ON);
481         Assertions.assertThrows(AssertionFailedError.class,
482                 () -> worker.equalsTests(new EqualsSetterThrowingBean(), new EqualsSetterThrowingBean()));
483     }
484 
485     /**
486      * Serialize.
487      *
488      * @param <T>
489      *            the generic type
490      * @param object
491      *            the object
492      *
493      * @return the t
494      *
495      * @throws Exception
496      *             the exception
497      */
498     @SuppressWarnings("unchecked")
499     private static <T> T serialize(final T object) throws Exception {
500         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
501         new ObjectOutputStream(baos).writeObject(object);
502 
503         final ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
504         return (T) new ObjectInputStream(bais).readObject();
505     }
506 
507     /**
508      * Test that a class with a setter-only property (no getter recognized by Introspector) triggers the boolean-wrapper
509      * patch branch in equalsTests (L633 true branch and L636 try block).
510      */
511     @Test
512     void testEqualsWithSetterOnlyProperty() {
513         var instance = new SetterOnlyBean();
514         var expected = new SetterOnlyBean();
515         JavaBeanTester.builder(SetterOnlyBean.class, null).testEquals(instance, expected);
516     }
517 
518     /**
519      * The Class SetterOnlyBean – has a setter but no getter so that equalsTests hits the getter==null patch (L633).
520      */
521     static class SetterOnlyBean {
522         /** The value – no getter, only a setter. */
523         @SuppressWarnings("unused")
524         private String value;
525 
526         /**
527          * Sets the value.
528          *
529          * @param value
530          *            the value
531          */
532         public void setValue(final String value) {
533             this.value = value;
534         }
535     }
536 
537     public static class GetterSetterThrowingBean {
538         private String value;
539 
540         public GetterSetterThrowingBean() {
541         }
542 
543         public String getValue() {
544             return this.value;
545         }
546 
547         public void setValue(final String value) {
548             throw new IllegalStateException("setter-failure");
549         }
550     }
551 
552     public static class ClearThrowingBean {
553         public ClearThrowingBean() {
554         }
555 
556         public void clear() {
557             throw new IllegalStateException("clear-failure");
558         }
559     }
560 
561     public static class ConstructorThrowingBean {
562         public ConstructorThrowingBean() {
563         }
564 
565         public ConstructorThrowingBean(final String value) {
566             throw new IllegalStateException(value);
567         }
568     }
569 
570     public static class SerializableWithFailingReadObject implements java.io.Serializable {
571         private static final long serialVersionUID = 1L;
572 
573         private String value = "ok";
574 
575         public SerializableWithFailingReadObject() {
576         }
577 
578         public String getValue() {
579             return this.value;
580         }
581 
582         public void setValue(final String value) {
583             this.value = value;
584         }
585 
586         @SuppressWarnings("unused")
587         private void readObject(final ObjectInputStream stream) throws IOException {
588             throw new IOException("deserialize-failure");
589         }
590     }
591 
592     public static class CopyFailureAfterLoadBean {
593         private static int setCalls;
594 
595         private String value;
596 
597         public CopyFailureAfterLoadBean() {
598         }
599 
600         static void resetSetCalls() {
601             setCalls = 0;
602         }
603 
604         public String getValue() {
605             return this.value;
606         }
607 
608         public void setValue(final String value) {
609             setCalls++;
610             if (setCalls >= 5) {
611                 throw new IllegalStateException("copy-failure");
612             }
613             this.value = value;
614         }
615 
616         @Override
617         public int hashCode() {
618             return Objects.hash(this.value);
619         }
620 
621         @Override
622         public boolean equals(final Object obj) {
623             if (this == obj) {
624                 return true;
625             }
626             if (!(obj instanceof CopyFailureAfterLoadBean)) {
627                 return false;
628             }
629             final CopyFailureAfterLoadBean other = (CopyFailureAfterLoadBean) obj;
630             return Objects.equals(this.value, other.value);
631         }
632     }
633 
634     public static class NoSetterBaseBean {
635         public NoSetterBaseBean() {
636         }
637     }
638 
639     public static class ExtensionCopyFailureBean {
640         public ExtensionCopyFailureBean() {
641         }
642 
643         public String getValue() {
644             return "value";
645         }
646 
647         public void setValue(final String value) {
648             throw new IllegalStateException("extension-copy-failure");
649         }
650     }
651 
652     public static class EqualsSetterThrowingBean {
653         private String value;
654 
655         public EqualsSetterThrowingBean() {
656         }
657 
658         public String getValue() {
659             return this.value;
660         }
661 
662         public void setValue(final String value) {
663             throw new IllegalStateException("equals-failure");
664         }
665     }
666 
667     /**
668      * Bean with no custom equals/hashCode – causes EqualsVerifier to fail.
669      */
670     static final class HashValue {
671         private final int hashCode;
672 
673         HashValue(final int hashCode) {
674             this.hashCode = hashCode;
675         }
676 
677         @Override
678         public int hashCode() {
679             return this.hashCode;
680         }
681     }
682 
683 }