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.junit.bct; 018 019import static java.util.Optional.*; 020import static org.apache.juneau.junit.bct.Utils.*; 021 022import java.text.*; 023import java.util.*; 024import java.util.function.*; 025 026/** 027 * Configuration and context object for advanced assertion operations. 028 * 029 * <p>This class encapsulates additional arguments and configuration options for assertion methods 030 * in the Bean-Centric Testing (BCT) framework. It provides a fluent API for customizing assertion 031 * behavior including custom converters and enhanced error messaging.</p> 032 * 033 * <p>The primary purposes of this class are:</p> 034 * <ul> 035 * <li><b>Custom Bean Conversion:</b> Override the default {@link BeanConverter} for specialized object introspection</li> 036 * <li><b>Enhanced Error Messages:</b> Add context-specific error messages with parameter substitution</li> 037 * <li><b>Fluent Configuration:</b> Chain configuration calls for readable test setup</li> 038 * <li><b>Assertion Context:</b> Provide additional context for complex assertion scenarios</li> 039 * </ul> 040 * 041 * <h5 class='section'>Basic Usage:</h5> 042 * <p class='bjava'> 043 * <jc>// Simple usage with default settings</jc> 044 * <jsm>assertBean</jsm>(<jsm>args</jsm>(), <jv>myBean</jv>, <js>"name,age"</js>, <js>"John,30"</js>); 045 * 046 * <jc>// Custom error message</jc> 047 * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"User validation failed"</js>), 048 * <jv>user</jv>, <js>"email,active"</js>, <js>"john@example.com,true"</js>); 049 * </p> 050 * 051 * <h5 class='section'>Custom Bean Converter:</h5> 052 * <p class='bjava'> 053 * <jc>// Use custom converter for specialized object handling</jc> 054 * <jk>var</jk> <jv>customConverter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 055 * .defaultSettings() 056 * .addStringifier(MyClass.<jk>class</jk>, <jp>obj</jp> -> <jp>obj</jp>.getDisplayName()) 057 * .build(); 058 * 059 * <jsm>assertBean</jsm>(<jsm>args</jsm>().setBeanConverter(<jv>customConverter</jv>), 060 * <jv>myCustomObject</jv>, <js>"property"</js>, <js>"expectedValue"</js>); 061 * </p> 062 * 063 * <h5 class='section'>Advanced Error Messages:</h5> 064 * <p class='bjava'> 065 * <jc>// Parameterized error messages</jc> 066 * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"Validation failed for user {0}"</js>, <jv>userId</jv>), 067 * <jv>user</jv>, <js>"status"</js>, <js>"ACTIVE"</js>); 068 * 069 * <jc>// Dynamic error message with supplier</jc> 070 * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(() -> <js>"Test failed at "</js> + Instant.<jsm>now</jsm>()), 071 * <jv>result</jv>, <js>"success"</js>, <js>"true"</js>); 072 * </p> 073 * 074 * <h5 class='section'>Fluent Configuration:</h5> 075 * <p class='bjava'> 076 * <jc>// Chain multiple configuration options</jc> 077 * <jk>var</jk> <jv>testArgs</jv> = args() 078 * .setBeanConverter(<jv>customConverter</jv>) 079 * .setMessage(<js>"Integration test failed for module {0}"</js>, <jv>moduleName</jv>); 080 * 081 * <jsm>assertBean</jsm>(<jv>testArgs</jv>, <jv>moduleConfig</jv>, <js>"enabled,version"</js>, <js>"true,2.1.0"</js>); 082 * <jsm>assertBeans</jsm>(<jv>testArgs</jv>, <jv>moduleList</jv>, <js>"name,status"</js>, 083 * <js>"ModuleA,ACTIVE"</js>, <js>"ModuleB,ACTIVE"</js>); 084 * </p> 085 * 086 * <h5 class='section'>Error Message Composition:</h5> 087 * <p>When assertion failures occur, error messages are intelligently composed:</p> 088 * <ul> 089 * <li><b>Base Message:</b> Custom message set via {@link #setMessage(String, Object...)} or {@link #setMessage(Supplier)}</li> 090 * <li><b>Assertion Context:</b> Specific context provided by individual assertion methods</li> 091 * <li><b>Composite Format:</b> <js>"{base message}, Caused by: {assertion context}"</js></li> 092 * </ul> 093 * 094 * <p class='bjava'> 095 * <jc>// Example error message composition:</jc> 096 * <jc>// Base: "User validation failed for user 123"</jc> 097 * <jc>// Context: "Bean assertion failed."</jc> 098 * <jc>// Result: "User validation failed for user 123, Caused by: Bean assertion failed."</jc> 099 * </p> 100 * 101 * <h5 class='section'>Thread Safety:</h5> 102 * <p>This class is <b>not thread-safe</b> and is intended for single-threaded test execution. 103 * Each test method should create its own instance using {@link BctAssertions#args()} or create 104 * a new instance directly with {@code new AssertionArgs()}.</p> 105 * 106 * <h5 class='section'>Immutability Considerations:</h5> 107 * <p>While this class uses fluent setters that return {@code this} for chaining, the instance 108 * is mutable. For reusable configurations across multiple tests, consider creating a factory 109 * method that returns pre-configured instances.</p> 110 * 111 * @see BctAssertions#args() 112 * @see BeanConverter 113 * @see BasicBeanConverter 114 */ 115public class AssertionArgs { 116 117 private BeanConverter beanConverter; 118 private Supplier<String> messageSupplier; 119 120 /** 121 * Creates a new instance with default settings. 122 * 123 * <p>Instances start with no custom bean converter and no custom error message. 124 * All assertion methods will use default behavior until configured otherwise.</p> 125 */ 126 public AssertionArgs() { /* no-op */ } 127 128 /** 129 * Sets a custom {@link BeanConverter} for object introspection and property access. 130 * 131 * <p>The custom converter allows fine-tuned control over how objects are converted to strings, 132 * how collections are listified, and how nested properties are accessed. This is particularly 133 * useful for:</p> 134 * <ul> 135 * <li><b>Custom Object Types:</b> Objects that don't follow standard JavaBean patterns</li> 136 * <li><b>Specialized Formatting:</b> Custom string representations for assertion comparisons</li> 137 * <li><b>Performance Optimization:</b> Cached or optimized property access strategies</li> 138 * <li><b>Domain-Specific Logic:</b> Business-specific property resolution rules</li> 139 * </ul> 140 * 141 * <h5 class='section'>Example:</h5> 142 * <p class='bjava'> 143 * <jc>// Create converter with custom stringifiers</jc> 144 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 145 * .defaultSettings() 146 * .addStringifier(LocalDate.<jk>class</jk>, <jp>date</jp> -> <jp>date</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>)) 147 * .addStringifier(Money.<jk>class</jk>, <jp>money</jp> -> <jp>money</jp>.getAmount().toPlainString()) 148 * .build(); 149 * 150 * <jc>// Use in assertions</jc> 151 * <jsm>assertBean</jsm>(<jsm>args</jsm>().setBeanConverter(<jv>converter</jv>), 152 * <jv>order</jv>, <js>"date,total"</js>, <js>"2023-12-01,99.99"</js>); 153 * </p> 154 * 155 * @param value The custom bean converter to use. If null, assertions will fall back to the default converter. 156 * @return This instance for method chaining. 157 */ 158 public AssertionArgs setBeanConverter(BeanConverter value) { 159 beanConverter = value; 160 return this; 161 } 162 163 /** 164 * Gets the configured bean converter, if any. 165 * 166 * @return An Optional containing the custom converter, or empty if using default behavior. 167 */ 168 protected Optional<BeanConverter> getBeanConverter() { 169 return ofNullable(beanConverter); 170 } 171 172 /** 173 * Sets a custom error message supplier for assertion failures. 174 * 175 * <p>The supplier allows for dynamic message generation, including context that may only 176 * be available at the time of assertion failure. This is useful for:</p> 177 * <ul> 178 * <li><b>Timestamps:</b> Including the exact time of failure</li> 179 * <li><b>Test State:</b> Including runtime state information</li> 180 * <li><b>Expensive Operations:</b> Deferring costly string operations until needed</li> 181 * <li><b>Conditional Messages:</b> Different messages based on runtime conditions</li> 182 * </ul> 183 * 184 * <h5 class='section'>Example:</h5> 185 * <p class='bjava'> 186 * <jc>// Dynamic message with timestamp</jc> 187 * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(() -> <js>"Test failed at "</js> + Instant.<jsm>now</jsm>()), 188 * <jv>result</jv>, <js>"status"</js>, <js>"SUCCESS"</js>); 189 * 190 * <jc>// Message with expensive computation</jc> 191 * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(() -> <js>"Failed after "</js> + computeTestDuration() + <js>" ms"</js>), 192 * <jv>response</jv>, <js>"error"</js>, <js>"null"</js>); 193 * </p> 194 * 195 * @param value The message supplier. Called only when an assertion fails. 196 * @return This instance for method chaining. 197 */ 198 public AssertionArgs setMessage(Supplier<String> value) { 199 messageSupplier = value; 200 return this; 201 } 202 203 /** 204 * Sets a parameterized error message for assertion failures. 205 * 206 * <p>This method uses {@link MessageFormat} to substitute parameters into the message template. 207 * The formatting occurs immediately when this method is called, not when the assertion fails.</p> 208 * 209 * <h5 class='section'>Parameter Substitution:</h5> 210 * <p>Uses standard MessageFormat patterns:</p> 211 * <ul> 212 * <li><code>{0}</code> - First parameter</li> 213 * <li><code>{1}</code> - Second parameter</li> 214 * <li><code>{0,number,#}</code> - Formatted number</li> 215 * <li><code>{0,date,short}</code> - Formatted date</li> 216 * </ul> 217 * 218 * <h5 class='section'>Examples:</h5> 219 * <p class='bjava'> 220 * <jc>// Simple parameter substitution</jc> 221 * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"User {0} validation failed"</js>, <jv>userId</jv>), 222 * <jv>user</jv>, <js>"active"</js>, <js>"true"</js>); 223 * 224 * <jc>// Multiple parameters</jc> 225 * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"Test {0} failed on iteration {1}"</js>, <jv>testName</jv>, <jv>iteration</jv>), 226 * <jv>result</jv>, <js>"success"</js>, <js>"true"</js>); 227 * 228 * <jc>// Number formatting</jc> 229 * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"Expected {0,number,#.##} but got different value"</js>, <jv>expectedValue</jv>), 230 * <jv>actual</jv>, <js>"value"</js>, <js>"123.45"</js>); 231 * </p> 232 * 233 * @param message The message template with MessageFormat placeholders. 234 * @param args The parameters to substitute into the message template. 235 * @return This instance for method chaining. 236 */ 237 public AssertionArgs setMessage(String message, Object... args) { 238 messageSupplier = fs(message, args); 239 return this; 240 } 241 242 /** 243 * Gets the base message supplier for composition with assertion-specific messages. 244 * 245 * @return The configured message supplier, or null if no custom message was set. 246 */ 247 protected Supplier<String> getMessage() { 248 return messageSupplier; 249 } 250 251 /** 252 * Composes the final error message by combining custom and assertion-specific messages. 253 * 254 * <p>This method implements the message composition strategy used throughout the assertion framework:</p> 255 * <ul> 256 * <li><b>No Custom Message:</b> Returns the assertion-specific message as-is</li> 257 * <li><b>With Custom Message:</b> Returns <code>"{custom}, Caused by: {assertion}"</code></li> 258 * </ul> 259 * 260 * <p>This allows tests to provide high-level context while preserving the specific 261 * technical details about what assertion failed.</p> 262 * 263 * @param msg The assertion-specific message template. 264 * @param args Parameters for the assertion-specific message. 265 * @return A supplier that produces the composed error message. 266 */ 267 protected Supplier<String> getMessage(String msg, Object...args) { 268 return messageSupplier == null ? fs(msg, args) : fs("{0}, Caused by: {1}", messageSupplier.get(), f(msg, args)); 269 } 270}