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.bct; 018 019import static java.util.stream.Collectors.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022import static org.apache.juneau.commons.utils.ThrowableUtils.*; 023import static org.apache.juneau.bct.BctUtils.*; 024import static org.junit.jupiter.api.Assertions.*; 025import static org.junit.jupiter.api.Assertions.assertNotNull; 026 027import java.util.*; 028import java.util.function.*; 029import java.util.stream.*; 030 031import org.apache.juneau.commons.utils.*; 032import org.opentest4j.*; 033 034/** 035 * Comprehensive utility class for Bean-Centric Tests (BCT) and general testing operations. 036 * 037 * <p>This class extends the functionality provided by the JUnit Assertions class, with particular emphasis 038 * on the Bean-Centric Testing (BCT) framework. BCT enables sophisticated assertion patterns for 039 * testing object properties, collections, maps, and complex nested structures with minimal code.</p> 040 * 041 * <h5 class='section'>Bean-Centric Testing (BCT) Framework:</h5> 042 * <p>The BCT framework consists of several key components:</p> 043 * <ul> 044 * <li><b>{@link BeanConverter}:</b> Core interface for object conversion and property access</li> 045 * <li><b>{@link BasicBeanConverter}:</b> Default implementation with extensible type handlers</li> 046 * <li><b>Assertion Methods:</b> High-level testing methods that leverage the converter framework</li> 047 * </ul> 048 * 049 * <h5 class='section'>Primary BCT Assertion Methods:</h5> 050 * <dl> 051 * <dt><b>{@link #assertBean(Object, String, String)}</b></dt> 052 * <dd>Tests object properties with nested syntax support and collection iteration</dd> 053 * 054 * <dt><b>{@link #assertBeans(Collection, String, String...)}</b></dt> 055 * <dd>Tests collections of objects by extracting and comparing specific fields</dd> 056 * 057 * <dt><b>{@link #assertMapped(Object, BiFunction, String, String)}</b></dt> 058 * <dd>Tests custom property access using BiFunction for non-standard objects</dd> 059 * 060 * <dt><b>{@link #assertList(List, Object...)}</b></dt> 061 * <dd>Tests list/collection elements with varargs for expected values</dd> 062 * </dl> 063 * 064 * <h5 class='section'>BCT Advanced Features:</h5> 065 * <ul> 066 * <li><b>Nested Property Syntax:</b> "address{street,city}" for testing nested objects</li> 067 * <li><b>Collection Iteration:</b> "#{address{street,city}}" syntax for testing all elements</li> 068 * <li><b>Universal Size Properties:</b> "length" and "size" work on all collection types</li> 069 * <li><b>Array/List Access:</b> Numeric indices for element-specific testing</li> 070 * <li><b>Method Chaining:</b> Fluent setters can be tested directly</li> 071 * <li><b>Direct Field Access:</b> Public fields accessed without getters</li> 072 * <li><b>Map Key Access:</b> Including special <js>"<null>"</js> syntax for null keys</li> 073 * </ul> 074 * 075 * <h5 class='section'>Converter Extensibility:</h5> 076 * <p>The BCT framework is built on the extensible {@link BasicBeanConverter} which allows:</p> 077 * <ul> 078 * <li><b>Custom Stringifiers:</b> Type-specific string conversion logic</li> 079 * <li><b>Custom Listifiers:</b> Collection-type conversion for iteration</li> 080 * <li><b>Custom Swappers:</b> Object transformation before conversion</li> 081 * <li><b>Custom PropertyExtractors:</b> Property extraction</li> 082 * <li><b>Configurable Settings:</b> Formatting, delimiters, and display options</li> 083 * </ul> 084 * 085 * <h5 class='section'>Usage Examples:</h5> 086 * 087 * <p><b>Basic Property Testing:</b></p> 088 * <p class='bjava'> 089 * <jc>// Test multiple properties</jc> 090 * <jsm>assertBean</jsm>(<jv>user</jv>, <js>"name,age,active"</js>, <js>"John,30,true"</js>); 091 * 092 * <jc>// Test nested properties - user has getAddress() returning Address with getStreet() and getCity()</jc> 093 * <jsm>assertBean</jsm>(<jv>user</jv>, <js>"name,address{street,city}"</js>, <js>"John,{123 Main St,Springfield}"</js>); 094 * </p> 095 * 096 * <p><b>Collection and Array Testing:</b></p> 097 * <p class='bjava'> 098 * <jc>// Test collection size and iterate over all elements - order has getItems() returning List<Product> where Product has getName()</jc> 099 * <jsm>assertBean</jsm>(<jv>order</jv>, <js>"items{length,#{name}}"</js>, <js>"{3,[{Laptop},{Phone},{Tablet}]}"</js>); 100 * 101 * <jc>// Test specific array elements - listOfData is a List<DataObject> where DataObject has getData()</jc> 102 * <jsm>assertBean</jsm>(<jv>listOfData</jv>, <js>"0{data},1{data}"</js>, <js>"{100},{200}"</js>); 103 * </p> 104 * 105 * <p><b>Collection Testing:</b></p> 106 * <p class='bjava'> 107 * <jc>// Test list elements</jc> 108 * <jsm>assertList</jsm>(tags, <js>"red"</js>, <js>"green"</js>, <js>"blue"</js>); 109 * 110 * <jc>// Test map entries using assertBean</jc> 111 * <jsm>assertBean</jsm>(<jv>config</jv>, <js>"timeout,retries"</js>, <js>"30000,3"</js>); 112 * </p> 113 * 114 * <p><b>Custom Property Access:</b></p> 115 * <p class='bjava'> 116 * <jc>// Test with custom accessor function</jc> 117 * <jsm>assertMapped</jsm>(<jv>myObject</jv>, (<jp>obj</jp>, <jp>prop</jp>) -> <jp>obj</jp>.getProperty(<jp>prop</jp>), 118 * <js>"prop1,prop2"</js>, <js>"value1,value2"</js>); 119 * </p> 120 * 121 * <h5 class='section'>Customizing the Default Converter:</h5> 122 * <p>The default bean converter can be customized on a per-thread basis using:</p> 123 * <ul> 124 * <li><b>{@link BctConfiguration#set(BeanConverter)}:</b> Set a custom converter for the current thread</li> 125 * <li><b>{@link BctConfiguration#clear()}:</b> Reset to the system default converter</li> 126 * </ul> 127 * 128 * <p class='bjava'> 129 * <jc>// Example: Set custom converter in @BeforeEach method</jc> 130 * <ja>@BeforeEach</ja> 131 * <jk>void</jk> <jsm>setUp</jsm>() { 132 * <jk>var</jk> <jv>customConverter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 133 * .defaultSettings() 134 * .addStringifier(LocalDate.<jk>class</jk>, <jp>date</jp> -> <jp>date</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>)) 135 * .addStringifier(MyType.<jk>class</jk>, <jp>obj</jp> -> <jp>obj</jp>.customFormat()) 136 * .build(); 137 * BctAssertions.<jsm>setConverter</jsm>(<jv>customConverter</jv>); 138 * } 139 * 140 * <jc>// All assertions in this test class now use the custom converter</jc> 141 * <ja>@Test</ja> 142 * <jk>void</jk> <jsm>testWithCustomConverter</jsm>() { 143 * <jsm>assertBean</jsm>(<jv>myObject</jv>, <js>"date,property"</js>, <js>"2023-12-01,value"</js>); 144 * } 145 * 146 * <jc>// Clean up in @AfterEach method</jc> 147 * <ja>@AfterEach</ja> 148 * <jk>void</jk> <jsm>tearDown</jsm>() { 149 * BctAssertions.<jsm>resetConverter</jsm>(); 150 * } 151 * </p> 152 * 153 * <p class='bjava'> 154 * <jc>// Example: Per-test method converter override</jc> 155 * <ja>@Test</ja> 156 * <jk>void</jk> <jsm>testSpecificFormat</jsm>() { 157 * <jk>var</jk> <jv>dateConverter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 158 * .defaultSettings() 159 * .addStringifier(LocalDateTime.<jk>class</jk>, <jp>dt</jp> -> <jp>dt</jp>.format(DateTimeFormatter.<jsf>ISO_DATE_TIME</jsf>)) 160 * .build(); 161 * BctAssertions.<jsm>setConverter</jsm>(<jv>dateConverter</jv>); 162 * <jkt>try</jkt> { 163 * <jsm>assertBean</jsm>(<jv>event</jv>, <js>"timestamp"</js>, <js>"2023-12-01T10:30:00"</js>); 164 * } <jkt>finally</jkt> { 165 * BctAssertions.<jsm>resetConverter</jsm>(); 166 * } 167 * } 168 * </p> 169 * 170 * <h5 class='section'>Performance and Thread Safety:</h5> 171 * <p>The BCT framework is designed for high performance with:</p> 172 * <ul> 173 * <li><b>Caching:</b> Type-to-handler mappings cached for fast lookup</li> 174 * <li><b>Thread Safety:</b> All operations are thread-safe for concurrent testing</li> 175 * <li><b>Thread-Local Storage:</b> Default converter is stored per-thread, allowing parallel test execution</li> 176 * <li><b>Minimal Allocation:</b> Efficient object reuse and minimal temporary objects</li> 177 * </ul> 178 * 179 * @see BeanConverter 180 * @see BasicBeanConverter 181 * @see BctConfiguration#set(BeanConverter) 182 * @see BctConfiguration#clear() 183 */ 184@SuppressWarnings({ 185 "java:S115", // Constants use UPPER_snakeCase naming convention 186 "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants 187}) 188public class BctAssertions { 189 190 /** 191 * Sets a custom bean converter for the current thread. 192 * 193 * <p>Thin passthrough to {@link BctConfiguration#set(BeanConverter)}, provided here since every 194 * BCT caller already statically imports {@link BctAssertions}.<jsm>*</jsm> and would otherwise need to 195 * separately reference {@link BctConfiguration} just to override the converter.</p> 196 * 197 * <h5 class='section'>Example:</h5> 198 * <p class='bjava'> 199 * <ja>@BeforeEach</ja> 200 * <jk>void</jk> <jsm>setUp</jsm>() { 201 * BctAssertions.<jsm>setConverter</jsm>(<jv>customConverter</jv>); 202 * } 203 * </p> 204 * 205 * @param converter The bean converter to use for the current thread. Must not be <jk>null</jk>. 206 * @throws IllegalArgumentException If converter is <jk>null</jk>. 207 * @see #resetConverter() 208 * @see BctConfiguration#set(BeanConverter) 209 */ 210 public static void setConverter(BeanConverter converter) { 211 BctConfiguration.set(converter); 212 } 213 214 /** 215 * Resets the bean converter for the current thread back to the system default. 216 * 217 * <p>Thin passthrough to {@link BctConfiguration#clear()}, provided here since every BCT caller 218 * already statically imports {@link BctAssertions}.<jsm>*</jsm> and would otherwise need to 219 * separately reference {@link BctConfiguration} just to reset the converter.</p> 220 * 221 * <h5 class='section'>Example:</h5> 222 * <p class='bjava'> 223 * <ja>@AfterEach</ja> 224 * <jk>void</jk> <jsm>tearDown</jsm>() { 225 * BctAssertions.<jsm>resetConverter</jsm>(); 226 * } 227 * </p> 228 * 229 * @see #setConverter(BeanConverter) 230 * @see BctConfiguration#clear() 231 */ 232 public static void resetConverter() { 233 BctConfiguration.clear(); 234 } 235 236 /** 237 * Asserts that the fields/properties on the specified bean are the specified values after being converted to strings. 238 * 239 * <p>Same as {@link #assertBean(Supplier, Object, String, String)} but without a custom message.</p> 240 * 241 * @param actual The bean object to test. Must not be null. 242 * @param fields Comma-delimited list of property names to test. Supports nested syntax with {}. 243 * @param expected Comma-delimited list of expected values. Must match the order of fields. 244 * @throws AssertionError if the bean is null or any property values don't match expected values 245 * @see #assertBean(Supplier, Object, String, String) 246 */ 247 public static void assertBean(Object actual, String fields, String expected) { 248 assertBean(null, actual, fields, expected); 249 } 250 251 /** 252 * Asserts that the fields/properties on the specified bean are the specified values after being converted to strings. 253 * 254 * <p>This is the primary method for Bean-Centric Tests (BCT), supporting extensive property validation 255 * patterns including nested objects, collections, arrays, method chaining, direct field access, collection iteration 256 * with <js>"#{property}"</js> syntax, and universal <js>"length"</js>/<js>"size"</js> properties for all collection types.</p> 257 * 258 * <p>The method uses the default converter (set via {@link BctConfiguration#set(BeanConverter)}) for object introspection 259 * and value extraction. The converter provides sophisticated property access through the {@link BeanConverter} 260 * interface, supporting multiple fallback mechanisms for accessing object properties and values.</p> 261 * 262 * <h5 class='section'>Basic Usage:</h5> 263 * <p class='bjava'> 264 * <jc>// Test multiple properties</jc> 265 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"prop1,prop2,prop3"</js>, <js>"val1,val2,val3"</js>); 266 * 267 * <jc>// Test single property</jc> 268 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"name"</js>, <js>"John"</js>); 269 * 270 * <jc>// With custom error message</jc> 271 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"name,age"</js>, <js>"John,30"</js>, () -> <js>"User validation failed"</js>); 272 * 273 * <jc>// With formatted message using Shorts.fs() for convenient message suppliers with arguments</jc> 274 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"name,age"</js>, <js>"John,30"</js>, <jsm>fs</jsm>(<js>"User %s validation failed"</js>, <js>"John"</js>)); 275 * </p> 276 * 277 * <h5 class='section'>Nested Property Testing:</h5> 278 * <p class='bjava'> 279 * <jc>// Test nested bean properties</jc> 280 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"name,address{street,city,state}"</js>, <js>"John,{123 Main St,Springfield,IL}"</js>); 281 * 282 * <jc>// Test arbitrarily deep nesting</jc> 283 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"name,person{address{geo{lat,lon}}}"</js>, <js>"John,{{{40.7,-74.0}}}"</js>); 284 * </p> 285 * 286 * <h5 class='section'>Array, List, and Stream Testing:</h5> 287 * <p class='bjava'> 288 * <jc>// Test array/list elements by index - items is a String[] or List<String></jc> 289 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"items{0,1,2}"</js>, <js>"{item1,item2,item3}"</js>); 290 * 291 * <jc>// Test nested properties within array elements - orders is a List<Order> where Order has getId() and getTotal()</jc> 292 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"orders{0{id,total}}"</js>, <js>"{{123,99.95}}"</js>); 293 * 294 * <jc>// Test array length property - items can be any array or collection type</jc> 295 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"items{length}"</js>, <js>"{5}"</js>); 296 * 297 * <jc>// Works with any iterable type including Streams - userStream returns a Stream<User> where User has getName()</jc> 298 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"userStream{#{name}}"</js>, <js>"[{Alice},{Bob}]"</js>); 299 * </p> 300 * 301 * <h5 class='section'>Collection Iteration Syntax:</h5> 302 * <p class='bjava'> 303 * <jc>// Test properties across ALL elements in a collection using #{...} syntax - userList is a List<User> where User has getName()</jc> 304 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"userList{#{name}}"</js>, <js>"[{John},{Jane},{Bob}]"</js>); 305 * 306 * <jc>// Test multiple properties from each element - orderList is a List<Order> where Order has getId() and getStatus()</jc> 307 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"orderList{#{id,status}}"</js>, <js>"[{123,ACTIVE},{124,PENDING}]"</js>); 308 * 309 * <jc>// Works with nested properties within each element - customers is a List<Customer> where Customer has getAddress() returning Address with getCity()</jc> 310 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"customers{#{address{city}}}"</js>, <js>"[{{New York}},{{Los Angeles}}]"</js>); 311 * 312 * <jc>// Works with arrays and any iterable collection type (including Streams)</jc> 313 * <jsm>assertBean</jsm>(<jv>config</jv>, <js>"itemArray{#{type}}"</js>, <js>"[{String},{Integer},{Boolean}]"</js>); 314 * <jsm>assertBean</jsm>(<jv>data</jv>, <js>"statusSet{#{name}}"</js>, <js>"[{ACTIVE},{PENDING},{CANCELLED}]"</js>); 315 * <jsm>assertBean</jsm>(<jv>processor</jv>, <js>"dataStream{#{value}}"</js>, <js>"[{A},{B},{C}]"</js>); 316 * </p> 317 * 318 * <h5 class='section'>Universal Collection Size Properties:</h5> 319 * <p class='bjava'> 320 * <jc>// Both 'length' and 'size' work universally across all collection types</jc> 321 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"myArray{length}"</js>, <js>"{5}"</js>); <jc>// Arrays</jc> 322 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"myArray{size}"</js>, <js>"{5}"</js>); <jc>// Also works for arrays</jc> 323 * 324 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"myList{size}"</js>, <js>"{3}"</js>); <jc>// Collections</jc> 325 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"myList{length}"</js>, <js>"{3}"</js>); <jc>// Also works for collections</jc> 326 * 327 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"myMap{size}"</js>, <js>"{7}"</js>); <jc>// Maps</jc> 328 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"myMap{length}"</js>, <js>"{7}"</js>); <jc>// Also works for maps</jc> 329 * </p> 330 * 331 * <h5 class='section'>Class Name Testing:</h5> 332 * <p class='bjava'> 333 * <jc>// Test class properties (prefer simple names for maintainability)</jc> 334 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"obj{class{simpleName}}"</js>, <js>"{{MyClass}}"</js>); 335 * 336 * <jc>// Test full class names when needed</jc> 337 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"obj{class{name}}"</js>, <js>"{{com.example.MyClass}}"</js>); 338 * </p> 339 * 340 * <h5 class='section'>Method Chaining Support:</h5> 341 * <p class='bjava'> 342 * <jc>// Test fluent setter chains (returns same object)</jc> 343 * <jsm>assertBean</jsm>( 344 * <jv>item</jv>.setType(<js>"foo"</js>).setFormat(<js>"bar"</js>).setDefault(<js>"baz"</js>), 345 * <js>"type,format,default"</js>, 346 * <js>"foo,bar,baz"</js> 347 * ); 348 * </p> 349 * 350 * <h5 class='section'>Advanced Collection Analysis:</h5> 351 * <p class='bjava'> 352 * <jc>// Combine size/length, metadata, and content iteration in single assertions - users is a List<User></jc> 353 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"users{length,class{simpleName},#{name}}"</js>, 354 * <js>"{3,{ArrayList},[{John},{Jane},{Bob}]}"</js>); 355 * 356 * <jc>// Comprehensive collection validation with multiple iteration patterns - items is a List<Product> where Product has getName() and getPrice()</jc> 357 * <jsm>assertBean</jsm>(<jv>order</jv>, <js>"items{size,#{name},#{price}}"</js>, 358 * <js>"{3,[{Laptop},{Phone},{Tablet}],[{999.99},{599.99},{399.99}]}"</js>); 359 * 360 * <jc>// Perfect for validation testing - verify error count and details; errors is a List<ValidationError> where ValidationError has getField() and getCode()</jc> 361 * <jsm>assertBean</jsm>(<jv>result</jv>, <js>"errors{length,#{field},#{code}}"</js>, 362 * <js>"{2,[{email},{password}],[{E001},{E002}]}"</js>); 363 * 364 * <jc>// Mixed collection types with consistent syntax - results and metadata are different collection types</jc> 365 * <jsm>assertBean</jsm>(<jv>response</jv>, <js>"results{size},metadata{length}"</js>, <js>"{25},{4}"</js>); 366 * </p> 367 * 368 * <h5 class='section'>Direct Field Access:</h5> 369 * <p class='bjava'> 370 * <jc>// Test public fields directly (no getters required)</jc> 371 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"f1,f2,f3"</js>, <js>"val1,val2,val3"</js>); 372 * 373 * <jc>// Test field properties with chaining</jc> 374 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"f1{length},f2{class{simpleName}}"</js>, <js>"{5},{{String}}"</js>); 375 * </p> 376 * 377 * <h5 class='section'>Map Testing:</h5> 378 * <p class='bjava'> 379 * <jc>// Test map values by key</jc> 380 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"configMap{timeout,retries}"</js>, <js>"{30000,3}"</js>); 381 * 382 * <jc>// Test map size</jc> 383 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"settings{size}"</js>, <js>"{5}"</js>); 384 * 385 * <jc>// Test null keys using special <null> syntax</jc> 386 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"mapWithNullKey{<null>}"</js>, <js>"{nullKeyValue}"</js>); 387 * </p> 388 * 389 * <h5 class='section'>Collection and Boolean Values:</h5> 390 * <p class='bjava'> 391 * <jc>// Test boolean values</jc> 392 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"enabled,visible"</js>, <js>"true,false"</js>); 393 * 394 * <jc>// Test enum collections</jc> 395 * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"statuses"</js>, <js>"[ACTIVE,PENDING]"</js>); 396 * </p> 397 * 398 * <h5 class='section'>Value Syntax Rules:</h5> 399 * <ul> 400 * <li><b>Simple values:</b> <js>"value"</js> for direct property values</li> 401 * <li><b>Nested values:</b> <js>"{value}"</js> for single-level nested properties</li> 402 * <li><b>Deep nested values:</b> <js>"{{value}}"</js>, <js>"{{{value}}}"</js> for multiple nesting levels</li> 403 * <li><b>Array/Collection values:</b> <js>"[item1,item2]"</js> for collections</li> 404 * <li><b>Collection iteration:</b> <js>"#{property}"</js> iterates over ALL collection elements, returns <js>"[{val1},{val2}]"</js></li> 405 * <li><b>Universal size properties:</b> <js>"length"</js> and <js>"size"</js> work on arrays, collections, and maps</li> 406 * <li><b>Boolean values:</b> <js>"true"</js>, <js>"false"</js></li> 407 * <li><b>Null values:</b> <js>"null"</js></li> 408 * </ul> 409 * 410 * <h5 class='section'>Property Access Priority:</h5> 411 * <ol> 412 * <li><b>Collection/Array access:</b> Numeric indices for arrays/lists (e.g., <js>"0"</js>, <js>"1"</js>)</li> 413 * <li><b>Universal size properties:</b> <js>"length"</js> and <js>"size"</js> for arrays, collections, and maps</li> 414 * <li><b>Map key access:</b> Direct key lookup for Map objects (including <js>"<null>"</js> for null keys)</li> 415 * <li><b>is{Property}()</b> methods (for boolean properties)</li> 416 * <li><b>get{Property}()</b> methods</li> 417 * <li><b>Public fields</b> (direct field access)</li> 418 * </ol> 419 * 420 * @param message Optional custom error message supplier. If provided, will be composed with the default assertion message. 421 * Use {@link Shorts#fs(String, Object...) Shorts.fs()} to conveniently 422 * create message suppliers with format arguments (e.g., <code>fs("User %s validation failed", userName)</code>). 423 * @param actual The bean object to test. Must not be null. 424 * @param fields Comma-delimited list of property names to test. Supports nested syntax with {}. 425 * @param expected Comma-delimited list of expected values. Must match the order of fields. 426 * @throws AssertionError if the bean is null or any property values don't match expected values 427 * @see BeanConverter 428 * @see BasicBeanConverter 429 * @see BctConfiguration#set(BeanConverter) 430 * @see Shorts#fs(String, Object...) 431 */ 432 public static void assertBean(Supplier<String> message, Object actual, String fields, String expected) { 433 assertNotNull(actual, "Actual was null."); 434 reqnn("fields", fields); 435 reqnn("expected", expected); 436 var converter = BctConfiguration.getConverter(); 437 assertEquals(expected, tokenize(fields).stream().map(x -> converter.getNested(actual, x)).collect(joining(",")), composeMessage(message, "Bean assertion failed.")); 438 } 439 440 /** 441 * Asserts that multiple beans in a collection have the expected property values. 442 * 443 * <p>This method validates that each bean in a collection has the specified property values, 444 * using the same property access logic as {@link #assertBean(Object, String, String)}. 445 * It's perfect for testing collections of similar objects or validation results.</p> 446 * 447 * <h5 class='section'>Basic Usage:</h5> 448 * <p class='bjava'> 449 * <jc>// Test list of user beans</jc> 450 * <jsm>assertBeans</jsm>(<jv>userList</jv>, <js>"name,age"</js>, 451 * <js>"John,25"</js>, <js>"Jane,30"</js>, <js>"Bob,35"</js>); 452 * </p> 453 * 454 * <h5 class='section'>Complex Property Testing:</h5> 455 * <p class='bjava'> 456 * <jc>// Test nested properties across multiple beans - orderList is a List<Order> where Order has getId() and getCustomer() returning Customer with getName() and getEmail()</jc> 457 * <jsm>assertBeans</jsm>(<jv>orderList</jv>, <js>"id,customer{name,email}"</js>, 458 * <js>"1,{John,john@example.com}"</js>, 459 * <js>"2,{Jane,jane@example.com}"</js>); 460 * 461 * <jc>// Test collection properties within beans - cartList is a List<ShoppingCart> where ShoppingCart has getItems() returning List<Product> and getTotal()</jc> 462 * <jsm>assertBeans</jsm>(<jv>cartList</jv>, <js>"items{0{name}},total"</js>, 463 * <js>"{{Laptop}},999.99"</js>, 464 * <js>"{{Phone}},599.99"</js>); 465 * </p> 466 * 467 * <h5 class='section'>Validation Testing:</h5> 468 * <p class='bjava'> 469 * <jc>// Test validation results</jc> 470 * <jsm>assertBeans</jsm>(<jv>validationErrors</jv>, <js>"field,message,code"</js>, 471 * <js>"email,Invalid email format,E001"</js>, 472 * <js>"age,Must be 18 or older,E002"</js>); 473 * </p> 474 * 475 * <h5 class='section'>Collection Iteration Testing:</h5> 476 * <p class='bjava'> 477 * <jc>// Test collection iteration within beans (#{...} syntax)</jc> 478 * <jsm>assertBeans</jsm>(<jv>departmentList</jv>, <js>"name,employees{#{name}}"</js>, 479 * <js>"Engineering,[{Alice},{Bob},{Charlie}]"</js>, 480 * <js>"Marketing,[{David},{Eve}]"</js>); 481 * </p> 482 * 483 * <h5 class='section'>Parser Result Testing:</h5> 484 * <p class='bjava'> 485 * <jc>// Test parsed object collections</jc> 486 * <jk>var</jk> <jv>parsed</jv> = JsonParser.<jsf>DEFAULT</jsf>.read(<jv>jsonArray</jv>, MyBean[].class); 487 * <jsm>assertBeans</jsm>(<jsm>l</jsm>(<jv>parsed</jv>), <js>"prop1,prop2"</js>, 488 * <js>"val1,val2"</js>, <js>"val3,val4"</js>); 489 * </p> 490 * 491 * @param actual The collection of beans to check. Must not be null. 492 * @param fields A comma-delimited list of bean property names (supports nested syntax). 493 * @param expected Array of expected value strings, one per bean. Each string contains comma-delimited values matching the fields. 494 * @throws AssertionError if the collection size doesn't match values array length or if any bean properties don't match 495 * @see #assertBean(Object, String, String) 496 */ 497 public static void assertBeans(Object actual, String fields, String...expected) { 498 assertBeans(null, actual, fields, expected); 499 } 500 501 /** 502 * Asserts that multiple beans in a collection have the expected property values. 503 * 504 * <p>Same as {@link #assertBeans(Object, String, String...)} but with a custom error message.</p> 505 * 506 * @param message Optional custom error message supplier. If provided, will be composed with the default assertion message. 507 * @param actual The collection of beans to check. Must not be null. 508 * @param fields A comma-delimited list of bean property names (supports nested syntax). 509 * @param expected Array of expected value strings, one per bean. Each string contains comma-delimited values matching the fields. 510 * @throws AssertionError if the collection size doesn't match values array length or if any bean properties don't match 511 * @see #assertBean(Object, String, String) 512 */ 513 public static void assertBeans(Supplier<String> message, Object actual, String fields, String...expected) { 514 assertNotNull(actual, "Value was null."); 515 reqnn("fields", fields); 516 reqnn("expected", expected); 517 518 var converter = BctConfiguration.getConverter(); 519 var tokens = tokenize(fields); 520 var errors = new ArrayList<AssertionFailedError>(); 521 List<Object> actualList = converter.listify(actual); 522 523 if (neq(expected.length, actualList.size())) { 524 errors.add(assertEqualsFailed(expected.length, actualList.size(), composeMessage(message, "Wrong number of beans."))); 525 } else { 526 for (var i = 0; i < actualList.size(); i++) { 527 var i2 = i; 528 var e = converter.stringify(expected[i]); 529 var a = tokens.stream().map(x -> converter.getNested(actualList.get(i2), x)).collect(joining(",")); 530 if (neq(e, a)) { 531 errors.add(assertEqualsFailed(e, a, composeMessage(message, "Bean at row <%s> did not match.", i))); 532 } 533 } 534 } 535 536 if (errors.isEmpty()) 537 return; 538 539 var actualStrings = actualList.stream().map(o -> tokens.stream().map(x -> converter.getNested(o, x)).collect(joining(","))).toList(); 540 541 throw assertEqualsFailed(Stream.of(expected).map(StringUtils::escapeForJava).collect(joining("\", \"", "\"", "\"")), 542 actualStrings.stream().map(StringUtils::escapeForJava).collect(joining("\", \"", "\"", "\"")), 543 composeMessage(message, "%s bean assertions failed:\n%s", errors.size(), errors.stream().map(x -> x.getMessage()).collect(joining("\n")))); 544 } 545 546 /** 547 * Asserts that the string representation of an object contains the expected substring. 548 * 549 * <p>Same as {@link #assertContains(Supplier, String, Object)} but without a custom message.</p> 550 * 551 * @param expected The substring that must be present in the actual object's string representation 552 * @param actual The object to test. Must not be null. 553 * @throws AssertionError if the actual object is null or its string representation doesn't contain the expected substring 554 * @see #assertContains(Supplier, String, Object) 555 */ 556 public static void assertContains(String expected, Object actual) { 557 assertContains(null, expected, actual); 558 } 559 560 /** 561 * Asserts that the string representation of an object contains the expected substring. 562 * 563 * <p>This method converts the actual object to its string representation using the current 564 * {@link BeanConverter} and then checks if it contains the expected substring. This is useful 565 * for testing partial content matches without requiring exact string equality.</p> 566 * 567 * <h5 class='section'>Usage Examples:</h5> 568 * <p class='bjava'> 569 * <jc>// Test that error message contains key information</jc> 570 * <jsm>assertContains</jsm>(<js>"FileNotFoundException"</js>, <jv>exception</jv>); 571 * 572 * <jc>// Test that object string representation contains expected data</jc> 573 * <jsm>assertContains</jsm>(<js>"status=ACTIVE"</js>, <jv>user</jv>); 574 * 575 * <jc>// Test partial JSON/XML content</jc> 576 * <jsm>assertContains</jsm>(<js>"\"name\":\"John\""</js>, <jv>jsonResponse</jv>); 577 * </p> 578 * 579 * @param message Optional custom error message supplier. If provided, will be composed with the default assertion message. 580 * @param expected The substring that must be present in the actual object's string representation 581 * @param actual The object to test. Must not be null. 582 * @throws AssertionError if the actual object is null or its string representation doesn't contain the expected substring 583 * @see #assertContainsAll(Object, String...) for multiple substring assertions 584 * @see #assertString(String, Object) for exact string matching 585 */ 586 public static void assertContains(Supplier<String> message, String expected, Object actual) { 587 reqnn("expected", expected); 588 reqnn("actual", actual); 589 assertNotNull(actual, "Value was null."); 590 591 var a = BctConfiguration.getConverter().stringify(actual); 592 assertTrue(a.contains(expected), composeMessage(message, "String did not contain expected substring. ==> expected: <%s> but was: <%s>", expected, a)); 593 } 594 595 /** 596 * Asserts that the string representation of an object does not contain the specified substring. 597 * 598 * <p>Same as {@link #assertNotContains(Supplier, String, Object)} but without a custom message.</p> 599 * 600 * @param unexpected The substring that must not be present in the actual object's string representation 601 * @param actual The object to test. Must not be null. 602 * @throws AssertionError if the actual object is null or its string representation contains the substring 603 * @see #assertNotContains(Supplier, String, Object) 604 */ 605 public static void assertNotContains(String unexpected, Object actual) { 606 assertNotContains(null, unexpected, actual); 607 } 608 609 /** 610 * Asserts that the string representation of an object does not contain the specified substring. 611 * 612 * <p>This is the negation of {@link #assertContains(Supplier, String, Object)}: the actual object is 613 * converted to its string representation using the current {@link BeanConverter} and must not contain 614 * the substring.</p> 615 * 616 * <h5 class='section'>Usage Examples:</h5> 617 * <p class='bjava'> 618 * <jc>// Test that a rendered page does not leak a value</jc> 619 * <jsm>assertNotContains</jsm>(<js>"password"</js>, <jv>html</jv>); 620 * </p> 621 * 622 * @param message Optional custom error message supplier. If provided, will be composed with the default assertion message. 623 * @param unexpected The substring that must not be present in the actual object's string representation 624 * @param actual The object to test. Must not be null. 625 * @throws AssertionError if the actual object is null or its string representation contains the substring 626 * @see #assertContains(Supplier, String, Object) 627 */ 628 public static void assertNotContains(Supplier<String> message, String unexpected, Object actual) { 629 reqnn("unexpected", unexpected); 630 reqnn("actual", actual); 631 assertNotNull(actual, "Value was null."); 632 633 var a = BctConfiguration.getConverter().stringify(actual); 634 assertFalse(a.contains(unexpected), composeMessage(message, "String contained unexpected substring. ==> unexpected: <%s> but was: <%s>", unexpected, a)); 635 } 636 637 /** 638 * Asserts that the string representation of an object contains all specified substrings. 639 * 640 * <p>Same as {@link #assertContainsAll(Supplier, Object, String...)} but without a custom message.</p> 641 * 642 * @param actual The object to test. Must not be null. 643 * @param expected Multiple substrings that must all be present in the actual object's string representation 644 * @throws AssertionError if the actual object is null or its string representation doesn't contain all expected substrings 645 * @see #assertContainsAll(Supplier, Object, String...) 646 */ 647 public static void assertContainsAll(Object actual, String...expected) { 648 assertContainsAll(null, actual, expected); 649 } 650 651 /** 652 * Asserts that the string representation of an object contains all specified substrings. 653 * 654 * <p>This method is similar to {@link #assertContains(String, Object)} but tests for multiple 655 * required substrings. All provided substrings must be present in the actual object's string 656 * representation for the assertion to pass.</p> 657 * 658 * <h5 class='section'>Usage Examples:</h5> 659 * <p class='bjava'> 660 * <jc>// Test that error contains multiple pieces of information</jc> 661 * <jsm>assertContainsAll</jsm>(<jv>exception</jv>, <js>"FileNotFoundException"</js>, <js>"config.xml"</js>, <js>"/etc"</js>); 662 * 663 * <jc>// Test that user object contains expected fields</jc> 664 * <jsm>assertContainsAll</jsm>(<jv>user</jv>, <js>"name=John"</js>, <js>"age=30"</js>, <js>"status=ACTIVE"</js>); 665 * 666 * <jc>// Test log output contains all required entries</jc> 667 * <jsm>assertContainsAll</jsm>(<jv>logOutput</jv>, <js>"INFO"</js>, <js>"Started"</js>, <js>"Successfully"</js>); 668 * </p> 669 * 670 * @param message Optional custom error message supplier. If provided, will be composed with the default assertion message. 671 * @param actual The object to test. Must not be null. 672 * @param expected Multiple substrings that must all be present in the actual object's string representation 673 * @throws AssertionError if the actual object is null or its string representation doesn't contain all expected substrings 674 * @see #assertContains(String, Object) for single substring assertions 675 */ 676 public static void assertContainsAll(Supplier<String> message, Object actual, String...expected) { 677 reqnn("expected", expected); 678 assertNotNull(actual, "Value was null."); 679 680 var a = BctConfiguration.getConverter().stringify(actual); 681 var errors = Stream.of(expected) 682 .filter(e -> ! a.contains(e)) 683 .map(e -> assertEqualsFailed(true, false, composeMessage(message, "String did not contain expected substring. ==> expected: <%s> but was: <%s>", e, a))) 684 .toList(); 685 686 if (errors.isEmpty()) 687 return; 688 689 if (errors.size() == 1) 690 throw errors.get(0); 691 692 var missingSubstrings = Stream.of(expected).filter(e -> ! a.contains(e)).toList(); 693 694 throw assertEqualsFailed(missingSubstrings.stream().map(StringUtils::escapeForJava).collect(joining("\", \"", "\"", "\"")), escapeForJava(a), 695 composeMessage(message, "%s substring assertions failed:\n%s", errors.size(), errors.stream().map(x -> x.getMessage()).collect(joining("\n")))); 696 } 697 698 /** 699 * Asserts that a collection-like object, Optional, Value, String, or array is not null and empty. 700 * 701 * <p>Same as {@link #assertEmpty(Supplier, Object)} but without a custom message.</p> 702 * 703 * @param value The object to test. Must not be null. 704 * @throws AssertionError if the object is null or not empty 705 * @see #assertEmpty(Supplier, Object) 706 */ 707 public static void assertEmpty(Object value) { 708 assertEmpty(null, value); 709 } 710 711 /** 712 * Asserts that a collection-like object, Optional, Value, String, or array is not null and empty. 713 * 714 * <p>This method validates that the provided object is empty according to its type:</p> 715 * <ul> 716 * <li><b>String:</b> Must have length 0</li> 717 * <li><b>Optional:</b> Must be empty (not present)</li> 718 * <li><b>Value:</b> Must be empty (value is null)</li> 719 * <li><b>Map:</b> Must have no entries</li> 720 * <li><b>Collection:</b> Must have no elements</li> 721 * <li><b>Array:</b> Must have length 0</li> 722 * <li><b>Other objects:</b> Must be convertible to an empty List via {@link BeanConverter#listify(Object)}</li> 723 * </ul> 724 * 725 * <h5 class='section'>Supported Types:</h5> 726 * <p>Any object that can be converted to a List, including:</p> 727 * <ul> 728 * <li>Collections (List, Set, Queue, etc.)</li> 729 * <li>Arrays (primitive and object arrays)</li> 730 * <li>Iterables, Iterators, Streams</li> 731 * <li>Maps (converted to list of entries)</li> 732 * <li>Optional objects</li> 733 * </ul> 734 * 735 * <h5 class='section'>Usage Examples:</h5> 736 * <p class='bjava'> 737 * <jc>// Test empty collections</jc> 738 * <jsm>assertEmpty</jsm>(Collections.<jsm>emptyList</jsm>()); 739 * <jsm>assertEmpty</jsm>(<jk>new</jk> ArrayList<>()); 740 * 741 * <jc>// Test empty arrays</jc> 742 * <jsm>assertEmpty</jsm>(<jk>new</jk> String[0]); 743 * 744 * <jc>// Test empty Optional</jc> 745 * <jsm>assertEmpty</jsm>(Optional.<jsm>empty</jsm>()); 746 * 747 * <jc>// Test empty Map</jc> 748 * <jsm>assertEmpty</jsm>(<jk>new</jk> HashMap<>()); 749 * </p> 750 * 751 * @param message Optional custom error message supplier. If provided, will be composed with the default assertion message. 752 * @param value The object to test. Must not be null. 753 * @throws AssertionError if the object is null or not empty 754 * @see #assertNotEmpty(Object) for testing non-empty collections 755 * @see #assertSize(int, Object) for testing specific sizes 756 */ 757 public static void assertEmpty(Supplier<String> message, Object value) { 758 assertNotNull(value, "Value was null."); 759 var size = BctConfiguration.getConverter().size(value); 760 assertEquals(0, size, composeMessage(message, "Value was not empty. Size=<%s>", size)); 761 } 762 763 /** 764 * Asserts that a List or List-like object contains the expected values using flexible comparison logic. 765 * 766 * <p>Same as {@link #assertList(Supplier, Object, Object...)} but without a custom message.</p> 767 * 768 * @param actual The List to test. Must not be null. 769 * @param expected Multiple arguments of expected values. 770 * Can be Strings (readable format comparison), Predicates (functional testing), or Objects (direct equality). 771 * @throws IllegalArgumentException if actual is null 772 * @throws AssertionError if the List size or contents don't match expected values 773 * @see #assertList(Supplier, Object, Object...) 774 */ 775 public static void assertList(Object actual, Object...expected) { 776 reqnn("actual", actual); 777 assertList(null, actual, expected); 778 } 779 780 /** 781 * Asserts that a List or List-like object contains the expected values using flexible comparison logic. 782 * 783 * <h5 class='section'>Testing Non-List Collections:</h5> 784 * <p class='bjava'> 785 * <jc>// Test a Set using l() conversion</jc> 786 * Set<String> <jv>mySet</jv> = <jk>new</jk> TreeSet<>(Arrays.<jsm>asList</jsm>(<js>"a"</js>, <js>"b"</js>, <js>"c"</js>)); 787 * <jsm>assertList</jsm>(<jsm>l</jsm>(<jv>mySet</jv>), <js>"a"</js>, <js>"b"</js>, <js>"c"</js>); 788 * 789 * <jc>// Test an array using l() conversion</jc> 790 * String[] <jv>myArray</jv> = {<js>"x"</js>, <js>"y"</js>, <js>"z"</js>}; 791 * <jsm>assertList</jsm>(<jsm>l</jsm>(<jv>myArray</jv>), <js>"x"</js>, <js>"y"</js>, <js>"z"</js>); 792 * 793 * <jc>// Test a Stream using l() conversion</jc> 794 * Stream<String> <jv>myStream</jv> = Stream.<jsm>of</jsm>(<js>"foo"</js>, <js>"bar"</js>); 795 * <jsm>assertList</jsm>(<jsm>l</jsm>(<jv>myStream</jv>), <js>"foo"</js>, <js>"bar"</js>); 796 * </p> 797 * 798 * <h5 class='section'>Comparison Modes:</h5> 799 * <p>The method supports three different ways to compare expected vs actual values:</p> 800 * 801 * <h6 class='section'>1. String Comparison (Readable Format):</h6> 802 * <p class='bjava'> 803 * <jc>// Elements are converted to strings using the bean converter and compared as strings</jc> 804 * <jsm>assertList</jsm>(List.<jsm>of</jsm>(1, 2, 3), <js>"1"</js>, <js>"2"</js>, <js>"3"</js>); 805 * <jsm>assertList</jsm>(List.<jsm>of</jsm>(<js>"a"</js>, <js>"b"</js>), <js>"a"</js>, <js>"b"</js>); 806 * </p> 807 * 808 * <h6 class='section'>2. Predicate Testing (Functional Validation):</h6> 809 * <p class='bjava'> 810 * <jc>// Use Predicate<T> for functional testing</jc> 811 * Predicate<Integer> <jv>greaterThanOne</jv> = <jv>x</jv> -> <jv>x</jv> > 1; 812 * <jsm>assertList</jsm>(List.<jsm>of</jsm>(2, 3, 4), <jv>greaterThanOne</jv>, <jv>greaterThanOne</jv>, <jv>greaterThanOne</jv>); 813 * 814 * <jc>// Mix predicates with other comparison types</jc> 815 * Predicate<String> <jv>startsWithA</jv> = <jv>s</jv> -> <jv>s</jv>.startsWith(<js>"a"</js>); 816 * <jsm>assertList</jsm>(List.<jsm>of</jsm>(<js>"apple"</js>, <js>"banana"</js>), <jv>startsWithA</jv>, <js>"banana"</js>); 817 * </p> 818 * 819 * <h6 class='section'>3. Object Equality (Direct Comparison):</h6> 820 * <p class='bjava'> 821 * <jc>// Non-String, non-Predicate objects use <jsm>Objects.equals</jsm>() comparison</jc> 822 * <jsm>assertList</jsm>(List.<jsm>of</jsm>(1, 2, 3), 1, 2, 3); <jc>// Integer objects</jc> 823 * <jsm>assertList</jsm>(List.<jsm>of</jsm>(<jv>myBean1</jv>, <jv>myBean2</jv>), <jv>myBean1</jv>, <jv>myBean2</jv>); <jc>// Custom objects</jc> 824 * </p> 825 * 826 * @param message Optional custom error message supplier. If provided, will be composed with the default assertion message. 827 * @param actual The List to test. Must not be null. 828 * @param expected Multiple arguments of expected values. 829 * Can be Strings (readable format comparison), Predicates (functional testing), or Objects (direct equality). 830 * @throws AssertionError if the List size or contents don't match expected values 831 */ 832 @SuppressWarnings({ 833 "java:S3776", // Cognitive complexity acceptable for this specific logic 834 "unchecked" // Type erasure requires unchecked casts 835 }) 836 public static void assertList(Supplier<String> message, Object actual, Object...expected) { 837 reqnn("expected", expected); 838 reqnn("actual", actual); 839 840 var converter = BctConfiguration.getConverter(); 841 List<Object> list = converter.listify(actual); 842 var errors = new ArrayList<AssertionFailedError>(); 843 844 if (neq(expected.length, list.size())) { 845 errors.add(assertEqualsFailed(expected.length, list.size(), composeMessage(message, "Wrong list length."))); 846 } else { 847 for (var i = 0; i < expected.length; i++) { 848 var x = list.get(i); 849 var e = expected[i]; 850 if (e instanceof String e2) { 851 if (neq(e2, converter.stringify(x))) { 852 errors.add(assertEqualsFailed(e2, converter.stringify(x), composeMessage(message, "Element at index %s did not match.", i))); 853 } 854 } else if (e instanceof Predicate<?> p) { 855 Predicate<Object> e2 = (Predicate<Object>) p; 856 if (! e2.test(x)) { 857 errors.add(new AssertionFailedError(composeMessage(message, "Element at index %s did not pass predicate. ==> actual: <%s>", i, converter.stringify(x)).get())); 858 } 859 } else { 860 if (neq(e, x)) { 861 errors.add(assertEqualsFailed(e, x, composeMessage(message, "Element at index %s did not match. ==> expected: <%s(%s)> but was: <%s(%s)>", i, e, cns(e), x, cns(x)))); 862 } 863 } 864 } 865 } 866 867 if (errors.isEmpty()) 868 return; 869 870 var actualStrings = list.stream().map(converter::stringify).toList(); 871 872 if (errors.size() == 1) 873 throw errors.get(0); 874 875 throw assertEqualsFailed(Stream.of(expected).map(converter::stringify).map(StringUtils::escapeForJava).collect(joining("\", \"", "[\"", "\"]")), 876 actualStrings.stream().map(StringUtils::escapeForJava).collect(joining("\", \"", "[\"", "\"]")), 877 composeMessage(message, "%s list assertions failed:\n%s", errors.size(), errors.stream().map(x -> x.getMessage()).collect(joining("\n")))); 878 } 879 880 /** 881 * Asserts that a Map contains the expected key/value pairs using flexible comparison logic. 882 * 883 * <p>Same as {@link #assertMap(Supplier, Map, Object...)} but without a custom message.</p> 884 * 885 * @param actual The Map to test. Must not be null. 886 * @param expected Multiple arguments of expected map entries. 887 * Can be Strings (readable format comparison), Predicates (functional testing), or Objects (direct equality). 888 * @throws AssertionError if the Map size or contents don't match expected values 889 * @see #assertMap(Supplier, Map, Object...) 890 */ 891 public static void assertMap(Map<?,?> actual, Object...expected) { 892 assertMap(null, actual, expected); 893 } 894 895 /** 896 * Asserts that a Map contains the expected key/value pairs using flexible comparison logic. 897 * 898 * <h5 class='section'>Map Entry Serialization:</h5> 899 * <p>Map entries are serialized to strings as key/value pairs in the format <js>"key=value"</js>. 900 * Nested maps and collections are supported with appropriate formatting.</p> 901 * 902 * <h5 class='section'>Testing Nested Maps and Collections:</h5> 903 * <p class='bjava'> 904 * <jc>// Test simple map entries</jc> 905 * Map<String,String> <jv>simpleMap</jv> = Map.<jsm>of</jsm>(<js>"a"</js>, <js>"1"</js>, <js>"b"</js>, <js>"2"</js>); 906 * <jsm>assertMap</jsm>(<jv>simpleMap</jv>, <js>"a=1"</js>, <js>"b=2"</js>); 907 * 908 * <jc>// Test nested maps</jc> 909 * Map<String,Map<String,Integer>> <jv>nestedMap</jv> = Map.<jsm>of</jsm>(<js>"a"</js>, Map.<jsm>of</jsm>(<js>"b"</js>, 1)); 910 * <jsm>assertMap</jsm>(<jv>nestedMap</jv>, <js>"a={b=1}"</js>); 911 * 912 * <jc>// Test maps with arrays/collections</jc> 913 * Map<String,Map<String,Integer[]>> <jv>mapWithArrays</jv> = Map.<jsm>of</jsm>(<js>"a"</js>, Map.<jsm>of</jsm>(<js>"b"</js>, <jk>new</jk> Integer[]{1,2})); 914 * <jsm>assertMap</jsm>(<jv>mapWithArrays</jv>, <js>"a={b=[1,2]}"</js>); 915 * </p> 916 * 917 * <h5 class='section'>Comparison Modes:</h5> 918 * <p>The method supports the same comparison modes as {@link #assertList(Object, Object...)}:</p> 919 * 920 * <h6 class='section'>1. String Comparison (Readable Format):</h6> 921 * <p class='bjava'> 922 * <jc>// Map entries are converted to strings and compared as strings</jc> 923 * <jsm>assertMap</jsm>(Map.<jsm>of</jsm>(<js>"key1"</js>, <js>"value1"</js>), <js>"key1=value1"</js>); 924 * <jsm>assertMap</jsm>(Map.<jsm>of</jsm>(<js>"count"</js>, 42), <js>"count=42"</js>); 925 * </p> 926 * 927 * <h6 class='section'>2. Predicate Testing (Functional Validation):</h6> 928 * <p class='bjava'> 929 * <jc>// Use Predicate<Map.Entry<K,V>> for functional testing</jc> 930 * Predicate<Map.Entry<String,Integer>> <jv>valueGreaterThanTen</jv> = <jv>entry</jv> -> <jv>entry</jv>.getValue() > 10; 931 * <jsm>assertMap</jsm>(Map.<jsm>of</jsm>(<js>"count"</js>, 42), <jv>valueGreaterThanTen</jv>); 932 * </p> 933 * 934 * <h6 class='section'>3. Object Equality (Direct Comparison):</h6> 935 * <p class='bjava'> 936 * <jc>// Non-String, non-Predicate objects use <jsm>Objects.equals</jsm>() comparison</jc> 937 * <jsm>assertMap</jsm>(Map.<jsm>of</jsm>(<js>"key"</js>, <jv>myObject</jv>), <jv>expectedEntry</jv>); 938 * </p> 939 * 940 * <h5 class='section'>Map Ordering Behavior:</h5> 941 * <p>The {@link Listifiers#mapListifier()} method ensures deterministic ordering for map entries:</p> 942 * <ul> 943 * <li><b>{@link SortedMap} (TreeMap, etc.):</b> Preserves existing sort order</li> 944 * <li><b>{@link LinkedHashMap}:</b> Preserves insertion order</li> 945 * <li><b>{@link HashMap} and other unordered Maps:</b> Converts to {@link TreeMap} for natural key ordering</li> 946 * </ul> 947 * <p>This ensures predictable test results regardless of the original map implementation.</p> 948 * 949 * @param message Optional custom error message supplier. If provided, will be composed with the default assertion message. 950 * @param actual The Map to test. Must not be null. 951 * @param expected Multiple arguments of expected map entries. 952 * Can be Strings (readable format comparison), Predicates (functional testing), or Objects (direct equality). 953 * @throws AssertionError if the Map size or contents don't match expected values 954 * @see #assertList(Supplier, Object, Object...) 955 */ 956 public static void assertMap(Supplier<String> message, Map<?,?> actual, Object...expected) { 957 assertList(message, actual, expected); 958 } 959 960 /** 961 * Asserts that mapped property access on an object returns expected values using a custom BiFunction. 962 * 963 * <p>This is designed for testing objects that don't follow 964 * standard JavaBean patterns or require custom property access logic. The BiFunction allows complete 965 * control over how properties are retrieved from the target object.</p> 966 * 967 * <p>This method creates an intermediate LinkedHashMap to collect all property values before 968 * using the same logic as assertBean for comparison. This ensures consistent ordering 969 * and supports the full nested property syntax. The {@link BasicBeanConverter#DEFAULT} is used 970 * for value stringification and nested property access.</p> 971 * 972 * @param <T> The type of object being tested 973 * @param message Optional custom error message supplier. If provided, will be composed with the default assertion message. 974 * @param actual The object to test properties on 975 * @param function The BiFunction that extracts property values. Receives (<jp>object</jp>, <jp>propertyName</jp>) and returns the property value. 976 * @param properties Comma-delimited list of property names to test 977 * @param expected Comma-delimited list of expected values (exceptions become simple class names) 978 * @throws AssertionError if any mapped property values don't match expected values 979 * @see #assertBean(Supplier, Object, String, String) 980 * @see BeanConverter 981 * @see BasicBeanConverter 982 */ 983 public static <T> void assertMapped(Supplier<String> message, T actual, BiFunction<T,String,Object> function, String properties, String expected) { 984 assertNotNull(actual, "Value was null."); 985 reqnn("function", function); 986 reqnn("properties", properties); 987 reqnn("expected", expected); 988 989 var m = new LinkedHashMap<String,Object>(); 990 for (var p : tokenize(properties)) { 991 var pv = p.getValue(); 992 m.put(pv, safe(() -> function.apply(actual, pv))); 993 } 994 995 assertBean(message, m, properties, expected); 996 } 997 998 /** 999 * Asserts that mapped property access on an object returns expected values using a custom BiFunction. 1000 * 1001 * <p>Same as {@link #assertMapped(Supplier, Object, BiFunction, String, String)} but without a custom message.</p> 1002 * 1003 * @param <T> The type of object being tested 1004 * @param actual The object to test properties on 1005 * @param function The BiFunction that extracts property values. Receives (<jp>object</jp>, <jp>propertyName</jp>) and returns the property value. 1006 * @param properties Comma-delimited list of property names to test 1007 * @param expected Comma-delimited list of expected values (exceptions become simple class names) 1008 * @throws AssertionError if any mapped property values don't match expected values 1009 * @see #assertMapped(Supplier, Object, BiFunction, String, String) 1010 */ 1011 public static <T> void assertMapped(T actual, BiFunction<T,String,Object> function, String properties, String expected) { 1012 assertMapped(null, actual, function, properties, expected); 1013 } 1014 1015 /** 1016 * Asserts that an object's string representation matches the specified glob-style pattern. 1017 * 1018 * <p>Same as {@link #assertMatchesGlob(Supplier, String, Object)} but without a custom message.</p> 1019 * 1020 * @param pattern The glob-style pattern to match against. 1021 * @param value The object to test. Must not be null. 1022 * @throws AssertionError if the value is null or its string representation doesn't match the pattern 1023 * @see #assertMatchesGlob(Supplier, String, Object) 1024 */ 1025 public static void assertMatchesGlob(String pattern, Object value) { 1026 assertMatchesGlob(null, pattern, value); 1027 } 1028 1029 /** 1030 * Asserts that an object's string representation matches the specified glob-style pattern. 1031 * 1032 * <p>This method converts the actual object to its string representation using the current 1033 * {@link BeanConverter} and then tests it against the provided glob-style pattern. 1034 * This is useful for testing string formats with simple wildcard patterns.</p> 1035 * 1036 * <h5 class='section'>Pattern Syntax:</h5> 1037 * <p>The pattern uses glob-style wildcards:</p> 1038 * <ul> 1039 * <li><b>{@code *}</b> matches any sequence of characters (including none)</li> 1040 * <li><b>{@code ?}</b> matches exactly one character</li> 1041 * <li><b>All other characters</b> are treated literally</li> 1042 * </ul> 1043 * 1044 * <h5 class='section'>Usage Examples:</h5> 1045 * <p class='bjava'> 1046 * <jc>// Test filename patterns</jc> 1047 * <jsm>assertMatchesGlob</jsm>(<js>"user_*_temp"</js>, <jv>filename</jv>); 1048 * 1049 * <jc>// Test single character wildcards</jc> 1050 * <jsm>assertMatchesGlob</jsm>(<js>"file?.txt"</js>, <jv>fileName</jv>); 1051 * 1052 * <jc>// Test combined patterns</jc> 1053 * <jsm>assertMatchesGlob</jsm>(<js>"log_*_?.txt"</js>, <jv>logFile</jv>); 1054 * </p> 1055 * 1056 * @param message Optional custom error message supplier. If provided, will be composed with the default assertion message. 1057 * @param pattern The glob-style pattern to match against. 1058 * @param value The object to test. Must not be null. 1059 * @throws AssertionError if the value is null or its string representation doesn't match the pattern 1060 * @see #assertString(Supplier, String, Object) for exact string matching 1061 * @see #assertContains(Supplier, String, Object) for substring matching 1062 */ 1063 public static void assertMatchesGlob(Supplier<String> message, String pattern, Object value) { 1064 reqnn("pattern", pattern); 1065 assertNotNull(value, "Value was null."); 1066 1067 var v = BctConfiguration.getConverter().stringify(value); 1068 var m = StringUtils.getGlobMatchPattern(pattern).matcher(v); 1069 assertTrue(m.matches(), composeMessage(message, "Pattern didn't match. ==> pattern: <%s> but was: <%s>", pattern, v)); 1070 } 1071 1072 /** 1073 * Asserts that a collection-like object, Optional, Value, String, or array is not null and not empty. 1074 * 1075 * <p>Same as {@link #assertNotEmpty(Supplier, Object)} but without a custom message.</p> 1076 * 1077 * @param value The object to test. Must not be null. 1078 * @throws AssertionError if the object is null or empty 1079 * @see #assertNotEmpty(Supplier, Object) 1080 */ 1081 public static void assertNotEmpty(Object value) { 1082 assertNotEmpty(null, value); 1083 } 1084 1085 /** 1086 * Asserts that a collection-like object, Optional, Value, String, or array is not null and not empty. 1087 * 1088 * <p>This method validates that the provided object is not empty according to its type:</p> 1089 * <ul> 1090 * <li><b>String:</b> Must have length > 0</li> 1091 * <li><b>Optional:</b> Must be present (not empty)</li> 1092 * <li><b>Value:</b> Must not be empty (value is not null)</li> 1093 * <li><b>Map:</b> Must have at least one entry</li> 1094 * <li><b>Collection:</b> Must have at least one element</li> 1095 * <li><b>Array:</b> Must have length > 0</li> 1096 * <li><b>Other objects:</b> Must convert to a non-empty List via {@link BeanConverter#listify(Object)}</li> 1097 * </ul> 1098 * 1099 * <h5 class='section'>Supported Types:</h5> 1100 * <p>Any object that can be converted to a List, including:</p> 1101 * <ul> 1102 * <li>Collections (List, Set, Queue, etc.)</li> 1103 * <li>Arrays (primitive and object arrays)</li> 1104 * <li>Iterables, Iterators, Streams</li> 1105 * <li>Maps (converted to list of entries)</li> 1106 * <li>Optional objects</li> 1107 * </ul> 1108 * 1109 * <h5 class='section'>Usage Examples:</h5> 1110 * <p class='bjava'> 1111 * <jc>// Test non-empty collections</jc> 1112 * <jsm>assertNotEmpty</jsm>(List.<jsm>of</jsm>(<js>"item1"</js>, <js>"item2"</js>)); 1113 * <jsm>assertNotEmpty</jsm>(<jk>new</jk> ArrayList<>(Arrays.<jsm>asList</jsm>(<js>"a"</js>))); 1114 * 1115 * <jc>// Test non-empty arrays</jc> 1116 * <jsm>assertNotEmpty</jsm>(<jk>new</jk> String[]{<js>"value"</js>}); 1117 * 1118 * <jc>// Test present Optional</jc> 1119 * <jsm>assertNotEmpty</jsm>(Optional.<jsm>of</jsm>(<js>"value"</js>)); 1120 * 1121 * <jc>// Test non-empty Map</jc> 1122 * <jsm>assertNotEmpty</jsm>(Map.<jsm>of</jsm>(<js>"key"</js>, <js>"value"</js>)); 1123 * </p> 1124 * 1125 * @param message Optional custom error message supplier. If provided, will be composed with the default assertion message. 1126 * @param value The object to test. Must not be null. 1127 * @throws AssertionError if the object is null or empty 1128 * @see #assertEmpty(Supplier, Object) for testing empty collections 1129 * @see #assertSize(Supplier, int, Object) for testing specific sizes 1130 */ 1131 public static void assertNotEmpty(Supplier<String> message, Object value) { 1132 assertNotNull(value, "Value was null."); 1133 int size = BctConfiguration.getConverter().size(value); 1134 assertTrue(size > 0, composeMessage(message, "Value was empty.")); 1135 } 1136 1137 /** 1138 * Asserts that a collection-like object or string is not null and of the specified size. 1139 * 1140 * <p>Same as {@link #assertSize(Supplier, int, Object)} but without a custom message.</p> 1141 * 1142 * @param expected The expected size/length. 1143 * @param actual The object to test. Must not be null. 1144 * @throws AssertionError if the object is null or not the expected size. 1145 * @see #assertSize(Supplier, int, Object) 1146 */ 1147 public static void assertSize(int expected, Object actual) { 1148 assertSize(null, expected, actual); 1149 } 1150 1151 /** 1152 * Asserts that a collection-like object or string is not null and of the specified size. 1153 * 1154 * <p>This method can validate the size of various types of objects:</p> 1155 * <ul> 1156 * <li><b>String:</b> Validates character length</li> 1157 * <li><b>Collection-like objects:</b> Any object that can be converted to a List via the underlying converter</li> 1158 * </ul> 1159 * 1160 * <h5 class='section'>Usage Examples:</h5> 1161 * <p class='bjava'> 1162 * <jc>// Test string length</jc> 1163 * <jsm>assertSize</jsm>(5, <js>"hello"</js>); 1164 * 1165 * <jc>// Test collection size</jc> 1166 * <jsm>assertSize</jsm>(3, List.<jsm>of</jsm>(<js>"a"</js>, <js>"b"</js>, <js>"c"</js>)); 1167 * 1168 * <jc>// Test array size</jc> 1169 * <jsm>assertSize</jsm>(2, <jk>new</jk> String[]{<js>"x"</js>, <js>"y"</js>}); 1170 * </p> 1171 * 1172 * @param message Optional custom error message supplier. If provided, will be composed with the default assertion message. 1173 * @param expected The expected size/length. 1174 * @param actual The object to test. Must not be null. 1175 * @throws AssertionError if the object is null or not the expected size. 1176 */ 1177 public static void assertSize(Supplier<String> message, int expected, Object actual) { 1178 assertNotNull(actual, "Value was null."); 1179 var size = BctConfiguration.getConverter().size(actual); 1180 assertEquals(expected, size, composeMessage(message, "Value not expected size.")); 1181 } 1182 1183 /** 1184 * Asserts that an object's string representation exactly matches the expected value. 1185 * 1186 * <p>Same as {@link #assertString(Supplier, String, Object)} but without a custom message.</p> 1187 * 1188 * @param expected The exact string that the actual object should convert to 1189 * @param actual The object to test. Must not be null. 1190 * @throws AssertionError if the actual object is null or its string representation doesn't exactly match expected 1191 * @see #assertString(Supplier, String, Object) 1192 */ 1193 public static void assertString(String expected, Object actual) { 1194 assertString(null, expected, actual); 1195 } 1196 1197 /** 1198 * Asserts that an object's string representation exactly matches the expected value. 1199 * 1200 * <p>This method converts the actual object to its string representation using the current 1201 * {@link BeanConverter} and performs an exact equality comparison with the expected string. 1202 * This is useful for testing complete string output, formatted objects, or converted values.</p> 1203 * 1204 * <h5 class='section'>Usage Examples:</h5> 1205 * <p class='bjava'> 1206 * <jc>// Test exact string conversion</jc> 1207 * <jsm>assertString</jsm>(<js>"John,30,true"</js>, <jv>user</jv>); <jc>// Assuming user converts to this format</jc> 1208 * 1209 * <jc>// Test formatted dates or numbers</jc> 1210 * <jsm>assertString</jsm>(<js>"2023-12-01"</js>, <jv>localDate</jv>); 1211 * 1212 * <jc>// Test complex object serialization</jc> 1213 * <jsm>assertString</jsm>(<js>"{name=John,age=30}"</js>, <jv>userMap</jv>); 1214 * 1215 * <jc>// Test array/collection formatting</jc> 1216 * <jsm>assertString</jsm>(<js>"[red,green,blue]"</js>, <jv>colors</jv>); 1217 * </p> 1218 * 1219 * @param message Optional custom error message supplier. If provided, will be composed with the default assertion message. 1220 * @param expected The exact string that the actual object should convert to 1221 * @param actual The object to test. Must not be null. 1222 * @throws AssertionError if the actual object is null or its string representation doesn't exactly match expected 1223 * @see #assertContains(Supplier, String, Object) for partial string matching 1224 * @see #assertMatchesGlob(Supplier, String, Object) for pattern-based matching 1225 */ 1226 public static void assertString(Supplier<String> message, String expected, Object actual) { 1227 assertNotNull(actual, "Value was null."); 1228 1229 var messageSupplier = message != null ? message : fs(""); 1230 assertEquals(expected, BctConfiguration.getConverter().stringify(actual), messageSupplier); 1231 } 1232 1233 /** 1234 * Composes an error message from an optional custom message and a default message. 1235 * 1236 * <p>If a custom message is provided, it is composed with the default message in the format: 1237 * <js>"{custom}, Caused by: {default}"</js>. Otherwise, the default message is returned.</p> 1238 * 1239 * @param customMessage Optional custom message supplier. Can be <jk>null</jk>, in which case the default message is returned unmodified. 1240 * @param defaultMessage Default message template. 1241 * @param defaultArgs Arguments for the default message template. 1242 * @return A supplier that produces the composed error message. 1243 */ 1244 private static Supplier<String> composeMessage(Supplier<String> customMessage, String defaultMessage, Object...defaultArgs) { 1245 if (customMessage == null) { 1246 return fs(defaultMessage, defaultArgs); 1247 } 1248 return fs("%s, Caused by: %s", customMessage.get(), f(defaultMessage, defaultArgs)); 1249 } 1250 1251 private BctAssertions() {} 1252}