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