View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau;
18  
19  import static org.apache.juneau.TestUtils.*;
20  import static org.junit.jupiter.api.Assertions.*;
21  
22  import org.apache.juneau.marshaller.*;
23  
24  /**
25   * Utility class for testing bean functionality in a standardized way.
26   * 
27   * <p>
28   * This class provides a fluent API for setting up and executing common bean tests including:
29   * <ul>
30   *    <li>Getter/setter validation using {@link TestUtils#assertBean(Object, String, String)}
31   *    <li>Copy constructor/method testing
32   *    <li>JSON serialization/deserialization testing
33   *    <li>Round-trip serialization testing
34   *    <li>toString() method validation
35   * </ul>
36   * 
37   * <h5 class='section'>Example Usage:</h5>
38   * <p class='bjava'>
39   *    BeanTester&lt;MyBean&gt; tester = testBean(myBean)
40   *       .json("{prop1:'value1',prop2:'value2'}")
41   *       .props("prop1,prop2")
42   *       .vals("'value1','value2'")
43   *       .string("{prop1:'value1',prop2:'value2'}");
44   *    
45   *    tester.assertGettersAndSetters();
46   *    tester.assertToJson();
47   *    tester.assertFromJson();
48   *    tester.assertRoundTrip();
49   *    tester.assertToString();
50   * </p>
51   * 
52   * <p>
53   * This class is typically used in nested test classes to provide consistent testing patterns
54   * across different bean types in the Apache Juneau project.
55   * 
56   * @param <T> The type of bean being tested
57   */
58  public class BeanTester<T> {
59  
60  	private T bean;
61  	private Class<T> c;
62  	
63  	/**
64  	 * Sets the bean instance to be tested.
65  	 * 
66  	 * @param value The bean instance to test.
67  	 * @return This object for method chaining.
68  	 */
69  	public BeanTester<T> bean(T value) { bean = value; c = (Class<T>) bean.getClass(); return this; }
70  	
71  	/**
72  	 * Returns the bean instance being tested.
73  	 * 
74  	 * @return The bean instance being tested.
75  	 */
76  	public T bean() { return bean; }
77  
78  	private String json;
79  	
80  	/**
81  	 * Sets the expected JSON representation of the bean.
82  	 * 
83  	 * <p>
84  	 * This JSON string is used by {@link #assertToJson()} to verify that the bean
85  	 * serializes to the expected JSON format, and by {@link #assertFromJson()} to
86  	 * verify that the JSON deserializes back to an equivalent bean.
87  	 * 
88  	 * @param value The expected JSON representation.
89  	 * @return This object for method chaining.
90  	 */
91  	public BeanTester<T> json(String value) { json = value; return this; }
92  	
93  	/**
94  	 * Returns the expected JSON representation of the bean.
95  	 * 
96  	 * @return The expected JSON representation.
97  	 */
98  	public String json() { return json; }
99  
100 	private String props;
101 	
102 	/**
103 	 * Sets the comma-delimited list of property names for bean validation.
104 	 * 
105 	 * <p>
106 	 * This string is used by {@link TestUtils#assertBean(Object, String, String)} to
107 	 * validate that the bean has the expected properties with the expected values.
108 	 * Property names should be listed in the same order as their corresponding values
109 	 * in the {@link #vals(String)} parameter.
110 	 * 
111 	 * @param value Comma-delimited list of property names (e.g., "prop1,prop2,prop3").
112 	 * @return This object for method chaining.
113 	 */
114 	public BeanTester<T> props(String value) { props = value; return this; }
115 	
116 	/**
117 	 * Returns the comma-delimited list of property names.
118 	 * 
119 	 * @return The comma-delimited list of property names.
120 	 */
121 	public String props() { return props; }
122 
123 	private String vals;
124 	
125 	/**
126 	 * Sets the comma-delimited list of expected property values for bean validation.
127 	 * 
128 	 * <p>
129 	 * This string is used by {@link TestUtils#assertBean(Object, String, String)} to
130 	 * validate that the bean properties have the expected values. Values should be
131 	 * listed in the same order as their corresponding property names in the
132 	 * {@link #props(String)} parameter. String values should be quoted (e.g., "'value'").
133 	 * 
134 	 * @param value Comma-delimited list of expected values (e.g., "'value1','value2',123").
135 	 * @return This object for method chaining.
136 	 */
137 	public BeanTester<T> vals(String value) { vals = value; return this; }
138 	
139 	/**
140 	 * Returns the comma-delimited list of expected property values.
141 	 * 
142 	 * @return The comma-delimited list of expected property values.
143 	 */
144 	public String vals() { return vals; }
145 
146 	private String string;
147 	
148 	/**
149 	 * Sets the expected string representation of the bean.
150 	 * 
151 	 * <p>
152 	 * This string is used by {@link #assertToString()} to verify that the bean's
153 	 * {@code toString()} method returns the expected value.
154 	 * 
155 	 * @param value The expected string representation.
156 	 * @return This object for method chaining.
157 	 */
158 	public BeanTester<T> string(String value) { string = value; return this; }
159 	
160 	/**
161 	 * Returns the expected string representation of the bean.
162 	 * 
163 	 * @return The expected string representation.
164 	 */
165 	public String string() { return string; }
166 
167 	/**
168 	 * Validates that the bean's getters and setters work correctly.
169 	 * 
170 	 * <p>
171 	 * This method uses {@link TestUtils#assertBean(Object, String, String)} to verify
172 	 * that the bean's properties match the expected values specified by {@link #props(String)}
173 	 * and {@link #vals(String)}.
174 	 * 
175 	 * @throws AssertionError if any property values don't match expectations.
176 	 */
177 	public void assertGettersAndSetters() {
178 		assertBean(bean, props, vals);
179 	}
180 
181 	/**
182 	 * Validates that the bean's copy constructor or copy() method works correctly.
183 	 * 
184 	 * <p>
185 	 * This method uses reflection to invoke the bean's {@code copy()} method, then verifies:
186 	 * <ul>
187 	 *    <li>The returned object is not the same instance as the original
188 	 *    <li>The returned object has the same property values as the original
189 	 * </ul>
190 	 * 
191 	 * @throws AssertionError if the copy is the same instance or has different property values.
192 	 * @throws RuntimeException if the bean doesn't have a {@code copy()} method or it fails to invoke.
193 	 */
194 	public void assertCopy() {
195 		var t = safe(()->bean.getClass().getMethod("copy").invoke(bean));
196 		assertNotSame(bean, t);
197 		assertBean(t, props, vals);
198 	}
199 
200 	/**
201 	 * Validates that the bean serializes to the expected JSON format.
202 	 * 
203 	 * <p>
204 	 * This method uses {@link TestUtils#assertJson(String, Object)} to verify that
205 	 * the bean serializes to the JSON string specified by {@link #json(String)}.
206 	 * 
207 	 * @throws AssertionError if the serialized JSON doesn't match the expected format.
208 	 */
209 	public void assertToJson() {
210 		assertJson(json, bean);
211 	}
212 
213 	/**
214 	 * Validates that the expected JSON deserializes to a bean with correct property values.
215 	 * 
216 	 * <p>
217 	 * This method deserializes the JSON string specified by {@link #json(String)} and
218 	 * verifies that the resulting bean has the expected property values specified by
219 	 * {@link #props(String)} and {@link #vals(String)}.
220 	 * 
221 	 * @throws AssertionError if the deserialized bean doesn't have the expected property values.
222 	 * @throws RuntimeException if JSON deserialization fails.
223 	 */
224 	public void assertFromJson() {
225 		assertBean(json(json, c), props, vals);
226 	}
227 
228 	/**
229 	 * Validates that the bean can be serialized to JSON and deserialized back correctly.
230 	 * 
231 	 * <p>
232 	 * This method performs a complete round-trip test by:
233 	 * <ol>
234 	 *    <li>Serializing the bean to JSON
235 	 *    <li>Deserializing the JSON back to a bean
236 	 *    <li>Verifying the resulting bean has the expected property values
237 	 * </ol>
238 	 * 
239 	 * @throws AssertionError if the round-trip bean doesn't have the expected property values.
240 	 * @throws RuntimeException if serialization or deserialization fails.
241 	 */
242 	public void assertRoundTrip() {
243 		assertBean(jsonRoundTrip(bean, c), props, vals);
244 	}
245 
246 	/**
247 	 * Validates that the bean's toString() method returns the expected string.
248 	 * 
249 	 * <p>
250 	 * This method compares the bean's {@code toString()} output with the string
251 	 * specified by {@link #string(String)}.
252 	 * 
253 	 * @throws AssertionError if the toString() output doesn't match the expected string.
254 	 */
255 	public void assertToString() {
256 		assertEquals(string, bean.toString());
257 	}
258 
259 	/**
260 	 * Serializes an object to JSON using the default sorted JSON serializer.
261 	 * 
262 	 * @param o The object to serialize.
263 	 * @return The JSON representation of the object.
264 	 */
265 	private static String json(Object o) {
266 		return Json5.DEFAULT_SORTED.write(o);
267 	}
268 
269 	/**
270 	 * Deserializes a JSON string to an object of the specified type.
271 	 * 
272 	 * @param <T> The type to deserialize to.
273 	 * @param o The JSON string to deserialize.
274 	 * @param c The class to deserialize to.
275 	 * @return The deserialized object.
276 	 */
277 	private static <T> T json(String o, Class<T> c) {
278 		return safe(()->Json5.DEFAULT_SORTED.read(o, c));
279 	}
280 
281 	/**
282 	 * Performs a round-trip serialization/deserialization test.
283 	 * 
284 	 * @param <T> The type of object to round-trip.
285 	 * @param o The object to serialize and deserialize.
286 	 * @param c The class to deserialize to.
287 	 * @return The object after round-trip serialization/deserialization.
288 	 */
289 	private static <T> T jsonRoundTrip(T o, Class<T> c) {
290 		return json(json(o), c);
291 	}
292 }