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