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