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<MyBean> 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 }