001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau;
018
019import static org.apache.juneau.commons.utils.ThrowableUtils.*;
020import static org.apache.juneau.marshall.marshaller.MarshallUtils.*;
021import static org.apache.juneau.bct.BctAssertions.*;
022import static org.junit.jupiter.api.Assertions.*;
023
024import org.apache.juneau.bct.*;
025
026/**
027 * Utility class for testing bean functionality in a standardized way.
028 *
029 * <p>
030 * This class provides a fluent API for setting up and executing common bean tests including:
031 * <ul>
032 *    <li>Getter/setter validation using {@link BctAssertions#assertBean(Object, String, String)}
033 *    <li>Copy constructor/method testing
034 *    <li>JSON serialization/deserialization testing
035 *    <li>Round-trip serialization testing
036 *    <li>toString() method validation
037 * </ul>
038 *
039 * <h5 class='section'>Example Usage:</h5>
040 * <p class='bjava'>
041 *    BeanTester&lt;MyBean&gt; tester = testBean(myBean)
042 *       .json("{prop1:'value1',prop2:'value2'}")
043 *       .props("prop1,prop2")
044 *       .vals("'value1','value2'")
045 *       .string("{prop1:'value1',prop2:'value2'}");
046 *
047 *    tester.assertGettersAndSetters();
048 *    tester.assertToJson();
049 *    tester.assertFromJson();
050 *    tester.assertRoundTrip();
051 *    tester.assertToString();
052 * </p>
053 *
054 * <p>
055 * This class is typically used in nested test classes to provide consistent testing patterns
056 * across different bean types in the Apache Juneau project.
057 *
058 * @param <T> The type of bean being tested
059 */
060@SuppressWarnings({
061   "java:S5960", // Test utility; JUnit assertions are its intentional purpose, not a production-code side effect.
062   "unchecked" // Class<T> cast for generic bean type in test utility
063})
064public class BeanTester<T> {
065
066   private T bean;
067   private Class<T> c;
068
069   /**
070    * Sets the bean instance to be tested.
071    *
072    * @param value The bean instance to test. Must not be <jk>null</jk>.
073    * @return This object for method chaining.
074    */
075   public BeanTester<T> bean(T value) { bean = value; c = (Class<T>) bean.getClass(); return this; }
076
077   /**
078    * Returns the bean instance being tested.
079    *
080    * @return The bean instance being tested.
081    */
082   public T bean() { return bean; }
083
084   private String json;
085
086   /**
087    * Sets the expected JSON representation of the bean.
088    *
089    * <p>
090    * This JSON string is used by {@link #assertToJson()} to verify that the bean
091    * serializes to the expected JSON format, and by {@link #assertFromJson()} to
092    * verify that the JSON deserializes back to an equivalent bean.
093    *
094    * @param value The expected JSON representation.
095    * @return This object for method chaining.
096    */
097   public BeanTester<T> json(String value) { json = value; return this; }
098
099   /**
100    * Returns the expected JSON representation of the bean.
101    *
102    * @return The expected JSON representation.
103    */
104   public String json() { return json; }
105
106   private String props;
107
108   /**
109    * Sets the comma-delimited list of property names for bean validation.
110    *
111    * <p>
112    * This string is used by {@link BctAssertions#assertBean(Object, String, String)} to
113    * validate that the bean has the expected properties with the expected values.
114    * Property names should be listed in the same order as their corresponding values
115    * in the {@link #vals(String)} parameter.
116    *
117    * @param value Comma-delimited list of property names (e.g., "prop1,prop2,prop3").
118    * @return This object for method chaining.
119    */
120   public BeanTester<T> props(String value) { props = value; return this; }
121
122   /**
123    * Returns the comma-delimited list of property names.
124    *
125    * @return The comma-delimited list of property names.
126    */
127   public String props() { return props; }
128
129   private String vals;
130
131   /**
132    * Sets the comma-delimited list of expected property values for bean validation.
133    *
134    * <p>
135    * This string is used by {@link BctAssertions#assertBean(Object, String, String)} to
136    * validate that the bean properties have the expected values. Values should be
137    * listed in the same order as their corresponding property names in the
138    * {@link #props(String)} parameter. String values should be quoted (e.g., "'value'").
139    *
140    * @param value Comma-delimited list of expected values (e.g., "'value1','value2',123").
141    * @return This object for method chaining.
142    */
143   public BeanTester<T> vals(String value) { vals = value; return this; }
144
145   /**
146    * Returns the comma-delimited list of expected property values.
147    *
148    * @return The comma-delimited list of expected property values.
149    */
150   public String vals() { return vals; }
151
152   private String string;
153
154   /**
155    * Sets the expected string representation of the bean.
156    *
157    * <p>
158    * This string is used by {@link #assertToString()} to verify that the bean's
159    * {@code toString()} method returns the expected value.
160    *
161    * @param value The expected string representation.
162    * @return This object for method chaining.
163    */
164   public BeanTester<T> string(String value) { string = value; return this; }
165
166   /**
167    * Returns the expected string representation of the bean.
168    *
169    * @return The expected string representation.
170    */
171   public String string() { return string; }
172
173   /**
174    * Validates that the bean's getters and setters work correctly.
175    *
176    * <p>
177    * This method uses {@link BctAssertions#assertBean(Object, String, String)} to verify
178    * that the bean's properties match the expected values specified by {@link #props(String)}
179    * and {@link #vals(String)}.
180    *
181    * @throws AssertionError if any property values don't match expectations.
182    */
183   public void assertGettersAndSetters() {
184      assertBean(bean, props, vals);
185   }
186
187   /**
188    * Validates that the bean's copy constructor or copy() method works correctly.
189    *
190    * <p>
191    * This method uses reflection to invoke the bean's {@code copy()} method, then verifies:
192    * <ul>
193    *    <li>The returned object is not the same instance as the original
194    *    <li>The returned object has the same property values as the original
195    * </ul>
196    *
197    * @throws AssertionError if the copy is the same instance or has different property values.
198    * @throws RuntimeException if the bean doesn't have a {@code copy()} method or it fails to invoke.
199    */
200   public void assertCopy() {
201      var t = safe(()->bean.getClass().getMethod("copy").invoke(bean));
202      assertNotSame(bean, t);
203      assertBean(t, props, vals);
204   }
205
206   /**
207    * Validates that the bean serializes to the expected JSON format.
208    *
209    * <p>
210    * This method verifies that the bean serializes (via JSON5) to the JSON string
211    * specified by {@link #json(String)}.
212    *
213    * @throws AssertionError if the serialized JSON doesn't match the expected format.
214    */
215   public void assertToJson() {
216      assertEquals(json, json5(bean));
217   }
218
219   /**
220    * Validates that the expected JSON deserializes to a bean with correct property values.
221    *
222    * <p>
223    * This method deserializes the JSON string specified by {@link #json(String)} and
224    * verifies that the resulting bean has the expected property values specified by
225    * {@link #props(String)} and {@link #vals(String)}.
226    *
227    * @throws AssertionError if the deserialized bean doesn't have the expected property values.
228    * @throws RuntimeException if JSON deserialization fails.
229    */
230   public void assertFromJson() {
231      assertBean(json(json, c), props, vals);
232   }
233
234   /**
235    * Validates that the bean can be serialized to JSON and deserialized back correctly.
236    *
237    * <p>
238    * This method performs a complete round-trip test by:
239    * <ol>
240    *    <li>Serializing the bean to JSON
241    *    <li>Deserializing the JSON back to a bean
242    *    <li>Verifying the resulting bean has the expected property values
243    * </ol>
244    *
245    * @throws AssertionError if the round-trip bean doesn't have the expected property values.
246    * @throws RuntimeException if serialization or deserialization fails.
247    */
248   public void assertRoundTrip() {
249      assertBean(jsonRoundTrip(bean, c), props, vals);
250   }
251
252   /**
253    * Validates that the bean's toString() method returns the expected string.
254    *
255    * <p>
256    * This method compares the bean's {@code toString()} output with the string
257    * specified by {@link #string(String)}.
258    *
259    * @throws AssertionError if the toString() output doesn't match the expected string.
260    */
261   public void assertToString() {
262      assertEquals(string, bean.toString());
263   }
264
265   /**
266    * Serializes an object to JSON using the default sorted JSON serializer.
267    *
268    * @param o The object to serialize.
269    * @return The JSON representation of the object.
270    */
271   private static String json(Object o) {
272      return json5(o);
273   }
274
275   /**
276    * Deserializes a JSON string to an object of the specified type.
277    *
278    * @param <T> The type to deserialize to.
279    * @param o The JSON string to deserialize.
280    * @param c The class to deserialize to.
281    * @return The deserialized object.
282    */
283   private static <T> T json(String o, Class<T> c) {
284      return safe(()->json5(o, c));
285   }
286
287   /**
288    * Performs a round-trip serialization/deserialization test.
289    *
290    * @param <T> The type of object to round-trip.
291    * @param o The object to serialize and deserialize.
292    * @param c The class to deserialize to.
293    * @return The object after round-trip serialization/deserialization.
294    */
295   private static <T> T jsonRoundTrip(T o, Class<T> c) {
296      return json(json(o), c);
297   }
298}