This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 1c68d340c Test modernization
1c68d340c is described below

commit 1c68d340c64750b84a37e2fcda28cbb964c0ce72
Author: James Bognar <james.bog...@salesforce.com>
AuthorDate: Wed Sep 10 08:38:01 2025 -0400

    Test modernization
---
 .../java/org/apache/juneau/NestedTokenizer.java    | 253 ++++++++
 .../src/test/java/org/apache/juneau/TestUtils.java | 174 +-----
 .../juneau/html/HtmlDocConfigAnnotation_Test.java  |   2 +-
 .../org/apache/juneau/junit/AssertionArgs.java     | 264 ++++++++
 .../apache/juneau/junit/AssertionArgs_Test.java    | 508 +++++++++++++++
 .../java/org/apache/juneau/junit/Assertions2.java  | 696 +++++++++++++++++++++
 .../test/java/org/apache/juneau/junit/Utils.java   | 114 +++-
 .../apache/juneau/objecttools/ObjectRest_Test.java |  41 +-
 8 files changed, 1886 insertions(+), 166 deletions(-)

diff --git a/juneau-utest/src/test/java/org/apache/juneau/NestedTokenizer.java 
b/juneau-utest/src/test/java/org/apache/juneau/NestedTokenizer.java
new file mode 100644
index 000000000..8cec94e8e
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/NestedTokenizer.java
@@ -0,0 +1,253 @@
+package org.apache.juneau;
+
+import static org.apache.juneau.NestedTokenizer.ParseState.*;
+import static java.util.Collections.*;
+import static java.util.stream.Collectors.*;
+
+import java.util.*;
+import java.util.function.*;
+
+/**
+ * Splits a nested comma-delimited string into a list of Token objects using a 
state machine parser.
+ *
+ * <p>This class parses complex nested structures with support for escaping 
and arbitrary nesting depth.
+ * The parser uses a finite state machine to handle different contexts during 
parsing.</p>
+ *
+ * <h5 class='section'>Supported Syntax:</h5>
+ * <ul>
+ *     <li><js>"foo"</js> - Single value token</li>
+ *     <li><js>"foo,bar"</js> - Multiple value tokens</li>
+ *     <li><js>"foo{a,b},bar"</js> - Token with nested values</li>
+ *     <li><js>"foo{a{a1,a2}},bar"</js> - Recursively nested values</li>
+ *     <li><js>"foo\\,bar"</js> - Escaped comma in value</li>
+ *     <li><js>"foo\\{bar\\}"</js> - Escaped braces in value</li>
+ * </ul>
+ *
+ * <h5 class='section'>State Machine:</h5>
+ * <p>The parser operates in several states:</p>
+ * <ul>
+ *     <li><b>PARSING_VALUE:</b> Reading a token value</li>
+ *     <li><b>PARSING_NESTED:</b> Reading nested content within braces</li>
+ *     <li><b>IN_ESCAPE:</b> Processing escaped character</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ *     <jc>// Simple tokens</jc>
+ *     var tokens = NestedTokenizer.splitNested(<js>"foo,bar,baz"</js>);
+ *     <jc>// tokens = [Token{value="foo"}, Token{value="bar"}, 
Token{value="baz"}]</jc>
+ *
+ *     <jc>// Nested tokens</jc>
+ *     var nested = 
NestedTokenizer.splitNested(<js>"user{name,email},config{timeout,retries}"</js>);
+ *     <jc>// nested[0] = Token{value="user", nested=[Token{value="name"}, 
Token{value="email"}]}</jc>
+ *     <jc>// nested[1] = Token{value="config", 
nested=[Token{value="timeout"}, Token{value="retries"}]}</jc>
+ * </p>
+ */
+public class NestedTokenizer {
+
+       /**
+        * Parser states for the finite state machine.
+        */
+       enum ParseState {
+               /** Parsing a token value outside of nested braces */
+               PARSING_VALUE,
+               /** Parsing nested content within braces */
+               PARSING_NESTED,
+               /** Processing an escaped character */
+               IN_ESCAPE
+       }
+
+       public static List<Token> tokenize(String in) {
+               if (in == null) throw new IllegalArgumentException("Input was 
null.");
+               if (in.isBlank()) throw new IllegalArgumentException("Input was 
empty.");
+
+               var length = in.length();
+               var pos = 0;
+               var state = PARSING_VALUE;
+               var currentValue = new StringBuilder();
+               var nestedDepth = 0;
+               var nestedStart = -1;
+               var tokens = new ArrayList<Token>();
+               var lastWasComma = false;
+               var justCompletedNested = false;
+
+               while (pos < length) {
+                       var c = in.charAt(pos);
+
+                       if (state == PARSING_VALUE) {
+                               if (c == '\\') {
+                                       state = IN_ESCAPE;
+                               } else if (c == ',') {
+                                       var value = 
currentValue.toString().trim();
+                                       // Add token unless it's empty and we 
just completed a nested token
+                                       if (!value.isEmpty() || 
tokens.isEmpty() || !justCompletedNested) {
+                                               tokens.add(new Token(value));
+                                       }
+                                       currentValue.setLength(0);
+                                       nestedStart = -1;
+                                       lastWasComma = true;
+                                       justCompletedNested = false;
+                                       pos = skipWhitespace(in, pos);
+                               } else if (c == '{') {
+                                       nestedStart = pos + 1;
+                                       nestedDepth = 1;
+                                       state = PARSING_NESTED;
+                               } else {
+                                       currentValue.append(c);
+                                       lastWasComma = false;
+                                       justCompletedNested = false;
+                               }
+                       } else if (state == PARSING_NESTED) {
+                               if (c == '\\') {
+                                       state = IN_ESCAPE;
+                               } else if (c == '{') {
+                                       nestedDepth++;
+                               } else if (c == '}') {
+                                       nestedDepth--;
+                                       if (nestedDepth == 0) {
+                                               var value = 
currentValue.toString().trim();
+                                               var nestedContent = 
in.substring(nestedStart, pos);
+                                               var token = new Token(value);
+                                               if 
(!nestedContent.trim().isEmpty()) {
+                                                       
token.setNested(tokenize(nestedContent));
+                                               }
+                                               tokens.add(token);
+                                               currentValue.setLength(0);
+                                               nestedStart = -1;
+                                               lastWasComma = false;  // Reset 
since we've completed a token
+                                               justCompletedNested = true;  // 
Flag that we just completed a nested token
+                                               pos = skipWhitespace(in, pos);
+                                               state = PARSING_VALUE;
+                                       }
+                               }
+                       } else if (state == IN_ESCAPE) {
+                               // Add the escaped character to current value 
only if we're parsing the main token value
+                               if (nestedDepth == 0) {
+                                       currentValue.append(c);
+                               }
+                               state = (nestedDepth > 0) ? PARSING_NESTED : 
PARSING_VALUE;
+                       }
+
+                       pos++;
+               }
+
+               // Add final token if we have content, or if input ended with 
comma, or if no tokens yet
+               var finalValue = currentValue.toString().trim();
+               if (!finalValue.isEmpty() || lastWasComma || tokens.isEmpty()) {
+                       tokens.add(new Token(finalValue));
+               }
+
+               return tokens;
+       }
+
+       private static int skipWhitespace(String input, int position) {
+               var length = input.length();
+               while (position + 1 < length && 
Character.isWhitespace(input.charAt(position + 1))) {
+                       position++;
+               }
+               return position;
+       }
+
+       /**
+        * Represents a parsed token with optional nested sub-tokens.
+        *
+        * <p>A Token contains a string value and may have nested tokens 
representing
+        * the content inside braces. Tokens support deep nesting for complex 
hierarchical structures.</p>
+        *
+        * <h5 class='section'>Structure:</h5>
+        * <ul>
+        *      <li><b>value:</b> The main token value (part before any 
braces)</li>
+        *      <li><b>nested:</b> Optional list of nested tokens (content 
within braces)</li>
+        * </ul>
+        *
+        * <h5 class='section'>Examples:</h5>
+        * <ul>
+        *      <li><js>"foo"</js> → <js>Token{value="foo", 
nested=null}</js></li>
+        *      <li><js>"foo{a,b}"</js> → <js>Token{value="foo", 
nested=[Token{value="a"}, Token{value="b"}]}</js></li>
+        * </ul>
+        */
+       public static class Token {
+
+               /** The main value of this token */
+               private final String value;
+
+               /** Nested tokens if this token has braced content, null 
otherwise */
+               private List<Token> nested;
+
+               /**
+                * Creates a new token with the specified value.
+                *
+                * @param value The token value
+                */
+               public Token(String value) {
+                       this.value = value != null ? value : "";
+               }
+
+               /**
+                * Returns the main value of this token.
+                *
+                * @return The token value
+                */
+               public String getValue() {
+                       return value;
+               }
+
+               /**
+                * Returns true if this token has nested content.
+                *
+                * @return true if nested tokens exist
+                */
+               public boolean hasNested() {
+                       return nested != null && !nested.isEmpty();
+               }
+
+               /**
+                * Returns an unmodifiable view of the nested tokens.
+                *
+                * @return unmodifiable list of nested tokens, or empty list if 
none
+                */
+               public List<Token> getNested() {
+                       return nested != null ? unmodifiableList(nested) : 
emptyList();
+               }
+
+               /**
+                * Sets the nested tokens for this token (package-private for 
tokenizer use).
+                *
+                * @param nested The list of nested tokens
+                */
+               void setNested(List<Token> nested) {
+                       this.nested = nested;
+               }
+
+               @Override
+               public String toString() {
+                       return hasNested() ? 
nested.stream().map(Object::toString).collect(joining(",",value + "{","}")) : 
value;
+               }
+
+               @Override
+               public boolean equals(Object o) {
+                       return (o instanceof Token o2) && eq(this, o2, 
(x,y)->x.value.equals(y.value) && eq(x.nested, y.nested));
+               }
+
+               @Override
+               public int hashCode() {
+                       return Objects.hash(value, nested);
+               }
+       }
+
+       
//---------------------------------------------------------------------------------------------
+       // Helper methods.
+       
//---------------------------------------------------------------------------------------------
+
+       private static <T,U> boolean eq(T o1, U o2, BiPredicate<T,U> test) {
+               if (o1 == null) { return o2 == null; }
+               if (o2 == null) { return false; }
+               if (o1 == o2) { return true; }
+               return test.test(o1, o2);
+       }
+
+       @SuppressWarnings("unlikely-arg-type")
+       private static <T,U> boolean eq(T o1, U o2) {
+               return Objects.equals(o1, o2);
+       }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/TestUtils.java 
b/juneau-utest/src/test/java/org/apache/juneau/TestUtils.java
index 1f6f06e84..7344e6e1a 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/TestUtils.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/TestUtils.java
@@ -34,7 +34,6 @@ import org.apache.juneau.rest.mock.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.xml.*;
 import org.junit.jupiter.api.*;
-import org.opentest4j.*;
 
 /**
  * Comprehensive utility class for Bean-Centric Tests (BCT) and general 
testing operations.
@@ -314,14 +313,7 @@ public class TestUtils extends Utils2 {
         * @see BasicBeanConverter
         */
        public static void assertBean(Object actual, String fields, String 
expected) {
-               assertBean(BasicBeanConverter.DEFAULT, actual, fields, 
expected);
-       }
-
-       public static void assertBean(BeanConverter converter, Object actual, 
String fields, String expected) {
-               assertNotNull(actual, "Value was null.");
-               assertArgNotNull("fields", fields);
-               assertArgNotNull("expected", expected);
-               assertEquals(expected, tokenize(fields).stream().map(x -> 
converter.getNested(actual, x)).collect(joining(",")));
+               Assertions2.assertBean(actual, fields, expected);
        }
 
        private static List<NestedTokenizer.Token> tokenize(String fields) {
@@ -386,70 +378,9 @@ public class TestUtils extends Utils2 {
         * @see #assertBean(Object, String, String)
         */
        public static void assertBeans(Object actual, String fields, 
String...expected) {
-               assertBeans(BasicBeanConverter.DEFAULT, actual, fields, 
expected);
+               Assertions2.assertBeans(actual, fields, expected);
        }
 
-       public static void assertBeans(BasicBeanConverter converter, Object 
actual, String fields, String...expected) {
-               assertNotNull(actual, "Value was null.");
-               assertArgNotNull("fields", fields);
-               assertArgNotNull("expected", expected);
-
-               var tokens = tokenize(fields);
-               var errors = new ArrayList<AssertionFailedError>();
-               var actualList = converter.listify(actual);
-
-               if (ne(expected.length, actualList.size())) {
-                       errors.add(assertionFailed(expected.length, 
actualList.size(), "Wrong number of beans."));
-               } else {
-                       for (var i = 0; i < actualList.size(); i++) {
-                               var i2 = i;
-                               var a = tokens.stream().map(x -> 
converter.getNested(actualList.get(i2), x)).collect(joining(","));
-                               if (ne(r(expected[i]), a)) {
-                                       
errors.add(assertionFailed(r(expected[i]), a, "Bean at row {0} did not match.", 
i));
-                               }
-                       }
-               }
-
-               if (errors.isEmpty()) return;
-
-               var actualStrings = new ArrayList<String>();
-               for (var o : actualList) {
-                       actualStrings.add(tokens.stream().map(x -> 
converter.getNested(o, x)).collect(joining(",")));
-               }
-
-               if (errors.size() == 1) throw errors.get(0);
-               throw assertionFailed(
-                       
Stream.of(expected).map(TestUtils::escapeForJava).collect(joining("\", \"", 
"\"", "\"")),
-                       
actualStrings.stream().map(TestUtils::escapeForJava).collect(joining("\", \"", 
"\"", "\"")),
-                       "{0} bean assertions failed: {1}", errors.size(), 
errors.stream().map(x -> x.getMessage()).collect(joining("\n"))
-               );
-       }
-
-       private static AssertionFailedError assertionFailed(Object expected, 
Object actual, String message, Object...args) {
-               return new AssertionFailedError(f(message, args) + f(" ==> 
expected: <{0}> but was: <{1}>", expected, actual), expected, actual);
-       }
-
-       private static String escapeForJava(String s) {
-               StringBuilder sb = new StringBuilder();
-               for (char c : s.toCharArray()) {
-                       switch (c) {
-                               case '\"': sb.append("\\\""); break;
-                               case '\\': sb.append("\\\\"); break;
-                               case '\n': sb.append("\\n"); break;
-                               case '\r': sb.append("\\r"); break;
-                               case '\t': sb.append("\\t"); break;
-                               case '\f': sb.append("\\f"); break;
-                               case '\b': sb.append("\\b"); break;
-                               default:
-                                       if (c < 0x20 || c > 0x7E) {
-                                               
sb.append(String.format("\\u%04x", (int)c));
-                                       } else {
-                                               sb.append(c);
-                                       }
-                       }
-               }
-               return sb.toString();
-       }
 
        /**
         * Asserts that a List contains the expected values using flexible 
comparison logic.
@@ -508,41 +439,14 @@ public class TestUtils extends Utils2 {
         * @see #l(Object) for converting other collection types to Lists
         */
        public static <T> void assertList(Object actual, Object...expected) {
-               assertList(BasicBeanConverter.DEFAULT, actual, expected);
-       }
-
-       @SuppressWarnings("unchecked")
-       public static <T> void assertList(BeanConverter converter, Object 
actual, Object...expected) {
-               assertNotNull(actual, "Value was null.");
-               assertArgNotNull("expected", expected);
-
-               var list = converter.listify(actual);
-               assertEquals(expected.length, list.size(), fms("Wrong list 
length.  expected={0}, actual={1}", expected.length, list.size()));
-
-               for (var i = 0; i < expected.length; i++) {
-                       var x = list.get(i);
-                       if (expected[i] instanceof String e) {
-                               assertEquals(e, converter.stringify(x), 
fms("Element at index {0} did not match.  expected={1}, actual={2}", i, e, 
converter.stringify(x)));
-                       } else if (expected[i] instanceof Predicate e) {
-                               assertTrue(e.test(x), fms("Element at index {0} 
did pass predicate.  actual={1}", i, converter.stringify(x)));
-                       } else {
-                               assertEquals(expected[i], x, fms("Element at 
index {0} did not match.  expected={1}({2}), actual={3}(4)", i, expected[i], 
t(expected[i]), x, t(x)));
-                       }
-               }
-       }
-
-       private static String t(Object o) {
-               return o == null ? null : o.getClass().getSimpleName();
+               Assertions2.assertList(actual, expected);
        }
 
        /**
         * Asserts an object matches the expected string after it's been made 
{@link Utils#r readable}.
         */
        public static void assertContains(String expected, Object actual) {
-               assertArgNotNull("expected", expected);
-               assertNotNull(actual, "Value was null.");
-               var a2 = r(actual);
-               assertTrue(a2.contains(expected), fms("String did not contain 
expected substring.  expected={0}, actual={1}", expected, a2));
+               Assertions2.assertContains(expected, actual);
        }
 
        /**
@@ -551,33 +455,15 @@ public class TestUtils extends Utils2 {
         * @param expected
         * @param actual
         */
-       public static void assertContainsAll(String expected, Object actual) {
-               assertArgNotNull("expected", expected);
-               assertNotNull(actual, "Value was null.");
-               var a2 = r(actual);
-               for (var e : splita(expected))
-                       assertTrue(a2.contains(e), fms("String did not contain 
expected substring.  expected={0}, actual={1}", e, a2));
+       public static void assertContainsAll(Object actual, String...expected) {
+               Assertions2.assertContainsAll(actual, expected);
        }
 
        /**
         * Asserts that a collection is not null and empty.
         */
        public static void assertEmpty(Object value) {
-               if (value instanceof Optional v2) {
-                       assertTrue(v2.isEmpty(), "Optional was not empty");
-                       return;
-               }
-               if (value instanceof Map v2) {
-                       assertTrue(v2.isEmpty(), "Map was not empty");
-                       return;
-               }
-               assertEmpty(BasicBeanConverter.DEFAULT, value);
-       }
-
-       public static void assertEmpty(BasicBeanConverter converter, Object 
value) {
-               assertNotNull(value, "Value was null.");
-               assertTrue(converter.canListify(value), fms("Value cannot be 
converted to a list.  Class={0}", value.getClass().getSimpleName()));
-               assertTrue(converter.listify(value).isEmpty(), "Value was not 
empty.");
+               Assertions2.assertEmpty(value);
        }
 
        public static void assertEqualsAll(Object...values) {
@@ -656,14 +542,7 @@ public class TestUtils extends Utils2 {
         * @see BasicBeanConverter
         */
        public static void assertMap(Map<?,?> actual, String fields, String 
expected) {
-               assertMap(BasicBeanConverter.DEFAULT, actual, fields, expected);
-       }
-
-       public static void assertMap(BeanConverter converter, Map<?,?> actual, 
String fields, String expected) {
-               assertNotNull(actual, "Value was null.");
-               assertArgNotNull("fields", fields);
-               assertArgNotNull("expected", expected);
-               assertEquals(expected, tokenize(fields).stream().map(x -> 
converter.getNested(actual, x)).collect(joining(",")));
+               Assertions2.assertBean(actual, fields, expected);
        }
 
        /**
@@ -693,24 +572,14 @@ public class TestUtils extends Utils2 {
         * @see BasicBeanConverter
         */
        public static <T> void assertMapped(T actual, 
BiFunction<T,String,Object> f, String properties, String expected) {
-               assertNotNull(actual, "Value was null.");
-               var m = new LinkedHashMap<String,Object>();
-               for (var p : split(properties)) {
-                       try {
-                               m.put(p, f.apply(actual, p));
-                       } catch (Exception e) {
-                               m.put(p, simpleClassNameOf(e));
-                       }
-               }
-               assertMap(m, properties, expected);
+               Assertions2.assertMapped(actual, f, properties, expected);
        }
 
        /**
         * Asserts that a collection is not null and not empty.
         */
        public static void assertNotEmpty(Object value) {
-               assertNotNull(value, "Value was null.");
-               assertFalse(toList(value).isEmpty(), "Value was empty.");
+               Assertions2.assertNotEmpty(value);
        }
 
        public static void assertNotEqualsAny(Object actual, Object...values) {
@@ -753,41 +622,28 @@ public class TestUtils extends Utils2 {
         * @throws AssertionError if the object is null or not the expected 
size.
         */
        public static void assertSize(int expected, Object actual) {
-               assertNotNull(actual, "Value was null.");
-               if (actual instanceof String a2) {
-                       assertEquals(expected, a2.length(), fms("Value not 
expected size.  Expected: {0}, Actual: {1}, Value: {2}", expected, a2.length(), 
a2));
-                       return;
-               }
-               assertEquals(expected, toList(actual).size(), fms("Value not 
expected size.  Expected: {0}, Actual: {1}", expected, toList(actual).size()));
+               Assertions2.assertSize(expected, actual);
        }
 
        /**
         * Asserts an object matches the expected string after it's been made 
{@link Utils#r readable}.
         */
        public static void assertString(String expected, Object actual) {
-               assertNotNull(actual, "Value was null.");
-               assertEquals(expected, r(actual));
+               Assertions2.assertString(expected, actual);
        }
 
        /**
         * Asserts value when stringified matches the specified pattern.
         */
-       public static Object assertMatches(String pattern, Object value) {
-               var m = getMatchPattern3(pattern).matcher(s(value));
-               if (! m.matches()) {
-                       var msg = "Pattern didn't match: 
\n\tExpected:\n"+pattern+"\n\tActual:\n"+value;
-                       System.err.println(msg);  // For easier debugging.
-                       fail(msg);
-               }
-               return value;
+       public static void assertMatches(String pattern, Object value) {
+               Assertions2.assertMatches(pattern, value);
        }
 
        /**
         * Asserts an object matches the expected string after it's been made 
{@link Utils#r readable}.
         */
        public static void assertString(String expected, Object actual, 
Supplier<String> messageSupplier) {
-               assertNotNull(actual, "Value was null.");
-               assertEquals(expected, r(actual), messageSupplier);
+               Assertions2.assertString(expected, actual);
        }
 
        public static <T extends Throwable> T assertThrowable(Class<? extends 
Throwable> expectedType, String expectedSubstring, T t) {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/html/HtmlDocConfigAnnotation_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/html/HtmlDocConfigAnnotation_Test.java
index 1ae77f1d9..6bc18ef9e 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/html/HtmlDocConfigAnnotation_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/html/HtmlDocConfigAnnotation_Test.java
@@ -314,7 +314,7 @@ class HtmlDocConfigAnnotation_Test extends SimpleTestBase {
                var al = AnnotationWorkList.of(sr, e.getAnnotationList());
                var x = 
HtmlDocSerializer.create().apply(al).build().getSession();
                var r = x.serialize(null).replaceAll("[\r\n]+", "|");
-               
assertContainsAll("<aside>xxx</aside>,<footer>xxx</footer>,<head>xxx,<style>@import
 \"xxx\"; xxx zzz</style>,<nav><ol><li>xxx</li></ol>xxx</nav>,<script>xxx| 
yyy|</script>", r);
+               assertContainsAll(r, 
"<aside>xxx</aside>","<footer>xxx</footer>","<head>xxx","<style>@import 
\"xxx\"; xxx zzz</style>","<nav><ol><li>xxx</li></ol>xxx</nav>","<script>xxx| 
yyy|</script>");
        }
 
        
//-----------------------------------------------------------------------------------------------------------------
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/junit/AssertionArgs.java 
b/juneau-utest/src/test/java/org/apache/juneau/junit/AssertionArgs.java
new file mode 100644
index 000000000..cdf2a1705
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/AssertionArgs.java
@@ -0,0 +1,264 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.junit;
+
+import static java.util.Optional.*;
+import static org.apache.juneau.junit.Utils.*;
+
+import java.text.*;
+import java.util.*;
+import java.util.function.*;
+
+/**
+ * Configuration and context object for advanced assertion operations.
+ *
+ * <p>This class encapsulates additional arguments and configuration options 
for assertion methods
+ * in the Bean-Centric Testing (BCT) framework. It provides a fluent API for 
customizing assertion
+ * behavior including custom converters and enhanced error messaging.</p>
+ *
+ * <p>The primary purposes of this class are:</p>
+ * <ul>
+ *     <li><b>Custom Bean Conversion:</b> Override the default {@link 
BeanConverter} for specialized object introspection</li>
+ *     <li><b>Enhanced Error Messages:</b> Add context-specific error messages 
with parameter substitution</li>
+ *     <li><b>Fluent Configuration:</b> Chain configuration calls for readable 
test setup</li>
+ *     <li><b>Assertion Context:</b> Provide additional context for complex 
assertion scenarios</li>
+ * </ul>
+ *
+ * <h5 class='section'>Basic Usage:</h5>
+ * <p class='bjava'>
+ *     <jc>// Simple usage with default settings</jc>
+ *     assertBean(args(), myBean, <js>"name,age"</js>, <js>"John,30"</js>);
+ *
+ *     <jc>// Custom error message</jc>
+ *     assertBean(args().setMessage(<js>"User validation failed"</js>),
+ *         user, <js>"email,active"</js>, <js>"j...@example.com,true"</js>);
+ * </p>
+ *
+ * <h5 class='section'>Custom Bean Converter:</h5>
+ * <p class='bjava'>
+ *     <jc>// Use custom converter for specialized object handling</jc>
+ *     var customConverter = BasicBeanConverter.builder()
+ *         .addStringifier(MyClass.class, obj -> obj.getDisplayName())
+ *         .build();
+ *
+ *     assertBean(args().setBeanConverter(customConverter),
+ *         myCustomObject, <js>"property"</js>, <js>"expectedValue"</js>);
+ * </p>
+ *
+ * <h5 class='section'>Advanced Error Messages:</h5>
+ * <p class='bjava'>
+ *     <jc>// Parameterized error messages</jc>
+ *     assertBean(args().setMessage(<js>"Validation failed for user {0}"</js>, 
userId),
+ *         user, <js>"status"</js>, <js>"ACTIVE"</js>);
+ *
+ *     <jc>// Dynamic error message with supplier</jc>
+ *     assertBean(args().setMessage(() -> <js>"Test failed at "</js> + 
Instant.now()),
+ *         result, <js>"success"</js>, <js>"true"</js>);
+ * </p>
+ *
+ * <h5 class='section'>Fluent Configuration:</h5>
+ * <p class='bjava'>
+ *     <jc>// Chain multiple configuration options</jc>
+ *     var testArgs = args()
+ *         .setBeanConverter(customConverter)
+ *         .setMessage(<js>"Integration test failed for module {0}"</js>, 
moduleName);
+ *
+ *     assertBean(testArgs, moduleConfig, <js>"enabled,version"</js>, 
<js>"true,2.1.0"</js>);
+ *     assertBeans(testArgs, moduleList, <js>"name,status"</js>,
+ *         <js>"ModuleA,ACTIVE"</js>, <js>"ModuleB,ACTIVE"</js>);
+ * </p>
+ *
+ * <h5 class='section'>Error Message Composition:</h5>
+ * <p>When assertion failures occur, error messages are intelligently 
composed:</p>
+ * <ul>
+ *     <li><b>Base Message:</b> Custom message set via {@link 
#setMessage(String, Object)} or {@link #setMessage(Supplier)}</li>
+ *     <li><b>Assertion Context:</b> Specific context provided by individual 
assertion methods</li>
+ *     <li><b>Composite Format:</b> <js>"{base message}, Caused by: {assertion 
context}"</js></li>
+ * </ul>
+ *
+ * <p class='bjava'>
+ *     <jc>// Example error message composition:</jc>
+ *     <jc>// Base: "User validation failed for user 123"</jc>
+ *     <jc>// Context: "Bean assertion failed."</jc>
+ *     <jc>// Result: "User validation failed for user 123, Caused by: Bean 
assertion failed."</jc>
+ * </p>
+ *
+ * <h5 class='section'>Thread Safety:</h5>
+ * <p>This class is <b>not thread-safe</b> and is intended for single-threaded 
test execution.
+ * Each test method should create its own instance using {@link 
Assertions2#args()} or create
+ * a new instance directly with {@code new AssertionArgs()}.</p>
+ *
+ * <h5 class='section'>Immutability Considerations:</h5>
+ * <p>While this class uses fluent setters that return {@code this} for 
chaining, the instance
+ * is mutable. For reusable configurations across multiple tests, consider 
creating a factory
+ * method that returns pre-configured instances.</p>
+ *
+ * @see Assertions2#args()
+ * @see BeanConverter
+ * @see BasicBeanConverter
+ */
+public class AssertionArgs {
+
+       private BeanConverter beanConverter;
+       private Supplier<String> messageSupplier;
+
+       /**
+        * Creates a new instance with default settings.
+        *
+        * <p>Instances start with no custom bean converter and no custom error 
message.
+        * All assertion methods will use default behavior until configured 
otherwise.</p>
+        */
+       public AssertionArgs() { /* no-op */ }
+
+       /**
+        * Sets a custom {@link BeanConverter} for object introspection and 
property access.
+        *
+        * <p>The custom converter allows fine-tuned control over how objects 
are converted to strings,
+        * how collections are listified, and how nested properties are 
accessed. This is particularly
+        * useful for:</p>
+        * <ul>
+        *     <li><b>Custom Object Types:</b> Objects that don't follow 
standard JavaBean patterns</li>
+        *     <li><b>Specialized Formatting:</b> Custom string representations 
for assertion comparisons</li>
+        *     <li><b>Performance Optimization:</b> Cached or optimized 
property access strategies</li>
+        *     <li><b>Domain-Specific Logic:</b> Business-specific property 
resolution rules</li>
+        * </ul>
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *     <jc>// Create converter with custom stringifiers</jc>
+        *     var converter = BasicBeanConverter.builder()
+        *         .addStringifier(LocalDate.class, date -> 
date.format(DateTimeFormatter.ISO_LOCAL_DATE))
+        *         .addStringifier(Money.class, money -> 
money.getAmount().toPlainString())
+        *         .build();
+        *
+        *     <jc>// Use in assertions</jc>
+        *     assertBean(args().setBeanConverter(converter),
+        *         order, <js>"date,total"</js>, <js>"2023-12-01,99.99"</js>);
+        * </p>
+        *
+        * @param value The custom bean converter to use. If null, assertions 
will fall back to the default converter.
+        * @return This instance for method chaining.
+        */
+       public AssertionArgs setBeanConverter(BeanConverter value) {
+               beanConverter = value;
+               return this;
+       }
+
+       /**
+        * Gets the configured bean converter, if any.
+        *
+        * @return An Optional containing the custom converter, or empty if 
using default behavior.
+        */
+       protected Optional<BeanConverter> getBeanConverter() {
+               return ofNullable(beanConverter);
+       }
+
+       /**
+        * Sets a custom error message supplier for assertion failures.
+        *
+        * <p>The supplier allows for dynamic message generation, including 
context that may only
+        * be available at the time of assertion failure. This is useful 
for:</p>
+        * <ul>
+        *     <li><b>Timestamps:</b> Including the exact time of failure</li>
+        *     <li><b>Test State:</b> Including runtime state information</li>
+        *     <li><b>Expensive Operations:</b> Deferring costly string 
operations until needed</li>
+        *     <li><b>Conditional Messages:</b> Different messages based on 
runtime conditions</li>
+        * </ul>
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *     <jc>// Dynamic message with timestamp</jc>
+        *     assertBean(args().setMessage(() -> <js>"Test failed at "</js> + 
Instant.now()),
+        *         result, <js>"status"</js>, <js>"SUCCESS"</js>);
+        *
+        *     <jc>// Message with expensive computation</jc>
+        *     assertBean(args().setMessage(() -> <js>"Failed after "</js> + 
computeTestDuration() + <js>" ms"</js>),
+        *         response, <js>"error"</js>, <js>"null"</js>);
+        * </p>
+        *
+        * @param value The message supplier. Called only when an assertion 
fails.
+        * @return This instance for method chaining.
+        */
+       public AssertionArgs setMessage(Supplier<String> value) {
+               messageSupplier = value;
+               return this;
+       }
+
+       /**
+        * Sets a parameterized error message for assertion failures.
+        *
+        * <p>This method uses {@link MessageFormat} to substitute parameters 
into the message template.
+        * The formatting occurs immediately when this method is called, not 
when the assertion fails.</p>
+        *
+        * <h5 class='section'>Parameter Substitution:</h5>
+        * <p>Uses standard MessageFormat patterns:</p>
+        * <ul>
+        *     <li><code>{0}</code> - First parameter</li>
+        *     <li><code>{1}</code> - Second parameter</li>
+        *     <li><code>{0,number,#}</code> - Formatted number</li>
+        *     <li><code>{0,date,short}</code> - Formatted date</li>
+        * </ul>
+        *
+        * <h5 class='section'>Examples:</h5>
+        * <p class='bjava'>
+        *     <jc>// Simple parameter substitution</jc>
+        *     assertBean(args().setMessage(<js>"User {0} validation 
failed"</js>, userId),
+        *         user, <js>"active"</js>, <js>"true"</js>);
+        *
+        *     <jc>// Multiple parameters</jc>
+        *     assertBean(args().setMessage(<js>"Test {0} failed on iteration 
{1}"</js>, testName, iteration),
+        *         result, <js>"success"</js>, <js>"true"</js>);
+        *
+        *     <jc>// Number formatting</jc>
+        *     assertBean(args().setMessage(<js>"Expected {0,number,#.##} but 
got different value"</js>, expectedValue),
+        *         actual, <js>"value"</js>, <js>"123.45"</js>);
+        * </p>
+        *
+        * @param message The message template with MessageFormat placeholders.
+        * @param args The parameters to substitute into the message template.
+        * @return This instance for method chaining.
+        */
+       public AssertionArgs setMessage(String message, Object... args) {
+               messageSupplier = () -> MessageFormat.format(message, args);
+               return this;
+       }
+
+       /**
+        * Gets the base message supplier for composition with 
assertion-specific messages.
+        *
+        * @return The configured message supplier, or null if no custom 
message was set.
+        */
+       protected Supplier<String> getMessage() {
+               return messageSupplier;
+       }
+
+       /**
+        * Composes the final error message by combining custom and 
assertion-specific messages.
+        *
+        * <p>This method implements the message composition strategy used 
throughout the assertion framework:</p>
+        * <ul>
+        *     <li><b>No Custom Message:</b> Returns the assertion-specific 
message as-is</li>
+        *     <li><b>With Custom Message:</b> Returns <code>"{custom}, Caused 
by: {assertion}"</code></li>
+        * </ul>
+        *
+        * <p>This allows tests to provide high-level context while preserving 
the specific
+        * technical details about what assertion failed.</p>
+        *
+        * @param msg The assertion-specific message template.
+        * @param args Parameters for the assertion-specific message.
+        * @return A supplier that produces the composed error message.
+        */
+       protected Supplier<String> getMessage(String msg, Object...args) {
+               return messageSupplier == null ? fs(msg, args) : fs("{0}, 
Caused by: {1}", messageSupplier.get(), f(msg, args));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/junit/AssertionArgs_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/junit/AssertionArgs_Test.java
new file mode 100644
index 000000000..033c5a8d9
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/AssertionArgs_Test.java
@@ -0,0 +1,508 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.junit;
+
+import static org.apache.juneau.junit.Assertions2.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.text.*;
+import java.time.*;
+import java.util.*;
+import java.util.function.*;
+
+import org.junit.jupiter.api.*;
+
+/**
+ * Unit tests for {@link AssertionArgs}.
+ *
+ * <p>Tests the configuration and behavior of the assertion arguments class 
including
+ * bean converter customization, error message composition, and fluent API 
functionality.</p>
+ */
+class AssertionArgs_Test {
+
+       // Test objects for assertions
+       static class TestBean {
+               private String name;
+               private int age;
+               private boolean active;
+
+               public TestBean(String name, int age, boolean active) {
+                       this.name = name;
+                       this.age = age;
+                       this.active = active;
+               }
+
+               public String getName() { return name; }
+               public int getAge() { return age; }
+               public boolean isActive() { return active; }
+       }
+
+       static class CustomObject {
+               private String value;
+
+               public CustomObject(String value) {
+                       this.value = value;
+               }
+
+               public String getValue() { return value; }
+
+               @Override
+               public String toString() {
+                       return "CustomObject[" + value + "]";
+               }
+       }
+
+       @Test
+       void a01_defaultConstruction() {
+               var args = new AssertionArgs();
+
+               // Should have no custom converter
+               assertTrue(args.getBeanConverter().isEmpty());
+
+               // Should have no custom message
+               assertNull(args.getMessage());
+       }
+
+       @Test
+       void a02_fluentAPIReturnsThis() {
+               var args = new AssertionArgs();
+               var mockConverter = createMockConverter();
+
+               // Fluent methods should return the same instance
+               assertSame(args, args.setBeanConverter(mockConverter));
+               assertSame(args, args.setMessage("test message"));
+               assertSame(args, args.setMessage(() -> "dynamic message"));
+       }
+
+       @Test
+       void b01_beanConverterConfiguration() {
+               var args = new AssertionArgs();
+               var mockConverter = createMockConverter();
+
+               // Initially empty
+               assertTrue(args.getBeanConverter().isEmpty());
+
+               // Set converter
+               args.setBeanConverter(mockConverter);
+               assertTrue(args.getBeanConverter().isPresent());
+               assertSame(mockConverter, args.getBeanConverter().get());
+
+               // Set to null should clear
+               args.setBeanConverter(null);
+               assertTrue(args.getBeanConverter().isEmpty());
+       }
+
+       @Test
+       void b02_customConverterInAssertion() {
+               // Create a mock custom converter for testing
+               var customConverter = createCustomConverter();
+
+               var args = args().setBeanConverter(customConverter);
+               var obj = new TestBeanWithCustomObject("test", new 
CustomObject("value"));
+
+               // Should use custom converter for stringification
+               assertBean(args, obj, "custom", "CUSTOM:value");
+       }
+
+       static class TestBeanWithCustomObject {
+               private String name;
+               private CustomObject custom;
+
+               public TestBeanWithCustomObject(String name, CustomObject 
custom) {
+                       this.name = name;
+                       this.custom = custom;
+               }
+
+               public String getName() { return name; }
+               public CustomObject getCustom() { return custom; }
+       }
+
+       @Test
+       void c01_messageSupplierConfiguration() {
+               var args = new AssertionArgs();
+
+               // Initially null
+               assertNull(args.getMessage());
+
+               // Set supplier
+               Supplier<String> supplier = () -> "test message";
+               args.setMessage(supplier);
+               assertNotNull(args.getMessage());
+               assertEquals("test message", args.getMessage().get());
+
+               // Set different supplier
+               args.setMessage(() -> "different message");
+               assertEquals("different message", args.getMessage().get());
+       }
+
+       @Test
+       void c02_parameterizedMessageConfiguration() {
+               var args = new AssertionArgs();
+
+               // Simple parameter substitution
+               args.setMessage("Hello {0}", "World");
+               assertEquals("Hello World", args.getMessage().get());
+
+               // Multiple parameters
+               args.setMessage("User {0} has {1} points", "John", 100);
+               assertEquals("User John has 100 points", 
args.getMessage().get());
+
+               // Number formatting
+               args.setMessage("Value: {0,number,#.##}", 123.456);
+               assertEquals("Value: 123.46", args.getMessage().get());
+       }
+
+       @Test
+       void c03_dynamicMessageSupplier() {
+               var counter = new int[1]; // Mutable counter for testing
+               var args = new AssertionArgs();
+
+               args.setMessage(() -> "Call #" + (++counter[0]));
+
+               // Each call should increment the counter
+               assertEquals("Call #1", args.getMessage().get());
+               assertEquals("Call #2", args.getMessage().get());
+               assertEquals("Call #3", args.getMessage().get());
+       }
+
+       @Test
+       void d01_messageCompositionWithoutCustomMessage() {
+               var args = new AssertionArgs();
+
+               // No custom message, should return assertion message as-is
+               var composedMessage = args.getMessage("Bean assertion failed");
+               assertEquals("Bean assertion failed", composedMessage.get());
+
+               // With parameters
+               var composedWithParams = args.getMessage("Element at index {0} 
did not match", 5);
+               assertEquals("Element at index 5 did not match", 
composedWithParams.get());
+       }
+
+       @Test
+       void d02_messageCompositionWithCustomMessage() {
+               var args = new AssertionArgs();
+               args.setMessage("User validation failed");
+
+               // Should compose: custom + assertion
+               var composedMessage = args.getMessage("Bean assertion failed");
+               assertEquals("User validation failed, Caused by: Bean assertion 
failed", composedMessage.get());
+
+               // With parameters in assertion message
+               var composedWithParams = args.getMessage("Element at index {0} 
did not match", 3);
+               assertEquals("User validation failed, Caused by: Element at 
index 3 did not match", composedWithParams.get());
+       }
+
+       @Test
+       void d03_messageCompositionWithParameterizedCustomMessage() {
+               var args = new AssertionArgs();
+               args.setMessage("Test {0} failed on iteration {1}", 
"UserValidation", 42);
+
+               var composedMessage = args.getMessage("Bean assertion failed");
+               assertEquals("Test UserValidation failed on iteration 42, 
Caused by: Bean assertion failed", composedMessage.get());
+       }
+
+       @Test
+       void d04_messageCompositionWithDynamicCustomMessage() {
+               var timestamp = Instant.now().toString();
+               var args = new AssertionArgs();
+               args.setMessage(() -> "Test failed at " + timestamp);
+
+               var composedMessage = args.getMessage("Bean assertion failed");
+               assertEquals("Test failed at " + timestamp + ", Caused by: Bean 
assertion failed", composedMessage.get());
+       }
+
+       @Test
+       void e01_fluentConfigurationChaining() {
+               var converter = createMockConverter();
+
+               // Chain multiple configurations
+               var args = new AssertionArgs()
+                       .setBeanConverter(converter)
+                       .setMessage("Integration test failed for module {0}", 
"AuthModule");
+
+               // Verify both configurations applied
+               assertTrue(args.getBeanConverter().isPresent());
+               assertSame(converter, args.getBeanConverter().get());
+               assertEquals("Integration test failed for module AuthModule", 
args.getMessage().get());
+       }
+
+       @Test
+       void e02_configurationOverwriting() {
+               var args = new AssertionArgs();
+               var converter1 = createMockConverter();
+               var converter2 = createMockConverter();
+
+               // Set initial values
+               args.setBeanConverter(converter1)
+                   .setMessage("First message");
+
+               // Overwrite with new values
+               args.setBeanConverter(converter2)
+                   .setMessage("Second message");
+
+               // Should have latest values
+               assertSame(converter2, args.getBeanConverter().get());
+               assertEquals("Second message", args.getMessage().get());
+       }
+
+       @Test
+       void f01_integrationWithAssertBean() {
+               var bean = new TestBean("John", 30, true);
+               var args = args().setMessage("User test failed");
+
+               // Should work with custom message
+               assertBean(args, bean, "name,age,active", "John,30,true");
+
+               // Test assertion failure message composition
+               var exception = assertThrows(AssertionError.class, () -> {
+                       assertBean(args, bean, "name", "Jane");
+               });
+
+               assertTrue(exception.getMessage().contains("User test failed"));
+               assertTrue(exception.getMessage().contains("Caused by:"));
+       }
+
+       @Test
+       void f02_integrationWithAssertBeans() {
+               var beans = List.of(
+                       new TestBean("Alice", 25, true),
+                       new TestBean("Bob", 35, false)
+               );
+               var args = args().setMessage("Batch validation failed");
+
+               // Should work with custom message
+               assertBeans(args, beans, "name,age", "Alice,25", "Bob,35");
+
+               // Test assertion failure message composition
+               var exception = assertThrows(AssertionError.class, () -> {
+                       assertBeans(args, beans, "name", "Charlie", "David");
+               });
+
+               assertTrue(exception.getMessage().contains("Batch validation 
failed"));
+               assertTrue(exception.getMessage().contains("Caused by:"));
+       }
+
+       @Test
+       void f03_integrationWithAssertList() {
+               var list = List.of("apple", "banana", "cherry");
+               var args = args().setMessage("List validation failed");
+
+               // Should work with custom message
+               assertList(args, list, "apple", "banana", "cherry");
+
+               // Test assertion failure message composition
+               var exception = assertThrows(AssertionError.class, () -> {
+                       assertList(args, list, "orange", "banana", "cherry");
+               });
+
+               assertTrue(exception.getMessage().contains("List validation 
failed"));
+               assertTrue(exception.getMessage().contains("Caused by:"));
+       }
+
+       @Test
+       void g01_edgeCaseNullValues() {
+               var args = new AssertionArgs();
+
+               // Null converter should work
+               args.setBeanConverter(null);
+               assertTrue(args.getBeanConverter().isEmpty());
+
+               // Null message supplier should work
+               args.setMessage((Supplier<String>) null);
+               assertNull(args.getMessage());
+       }
+
+       @Test
+       void g02_edgeCaseEmptyMessages() {
+               var args = new AssertionArgs();
+
+               // Empty string message
+               args.setMessage("");
+               assertEquals("", args.getMessage().get());
+
+               // Empty supplier result
+               args.setMessage(() -> "");
+               assertEquals("", args.getMessage().get());
+
+               // Composition with empty custom message
+               var composedMessage = args.getMessage("Bean assertion failed");
+               assertEquals(", Caused by: Bean assertion failed", 
composedMessage.get());
+       }
+
+       @Test
+       void g03_edgeCaseComplexParameterFormatting() {
+               var args = new AssertionArgs();
+               var date = new Date();
+
+               // Date formatting
+               args.setMessage("Test executed on {0,date,short}", date);
+               var expectedDatePart = 
DateFormat.getDateInstance(DateFormat.SHORT).format(date);
+               assertTrue(args.getMessage().get().contains(expectedDatePart));
+
+               // Complex number formatting
+               args.setMessage("Processing {0,number,percent} complete", 0.85);
+               assertTrue(args.getMessage().get().contains("85%"));
+       }
+
+       @Test
+       void h01_threadSafetyDocumentationCompliance() {
+               // This test documents that AssertionArgs is NOT thread-safe
+               // Each thread should create its own instance
+
+               var sharedArgs = new AssertionArgs();
+               var results = Collections.synchronizedList(new 
ArrayList<String>());
+
+               // Simulate multiple threads modifying the same instance
+               var threads = new Thread[5];
+               for (int i = 0; i < threads.length; i++) {
+                       final int threadId = i;
+                       threads[i] = new Thread(() -> {
+                               sharedArgs.setMessage("Thread " + threadId + " 
message");
+                               // Small delay to increase chance of race 
condition
+                               try { Thread.sleep(1); } catch 
(InterruptedException e) {}
+                               results.add(sharedArgs.getMessage().get());
+                       });
+               }
+
+               // Start all threads
+               for (var thread : threads) {
+                       thread.start();
+               }
+
+               // Wait for completion
+               for (var thread : threads) {
+                       try { thread.join(); } catch (InterruptedException e) {}
+               }
+
+               // Due to race conditions, we may not get the expected messages
+               // This demonstrates why each test should create its own 
instance
+               assertEquals(5, results.size());
+               // Note: We don't assert specific values due to race conditions
+       }
+
+       @Test
+       void h02_recommendedUsagePattern() {
+               // Demonstrate the recommended pattern: create new instance per 
test
+
+               // Test 1: User validation
+               var userArgs = args().setMessage("User validation test");
+               var user = new TestBean("Alice", 25, true);
+               assertBean(userArgs, user, "name,active", "Alice,true");
+
+               // Test 2: Product validation (separate instance)
+               var productArgs = args().setMessage("Product validation test");
+               var products = List.of("Laptop", "Phone", "Tablet");
+               assertList(productArgs, products, "Laptop", "Phone", "Tablet");
+
+               // Each test has its own configuration without interference
+               assertEquals("User validation test", 
userArgs.getMessage().get());
+               assertEquals("Product validation test", 
productArgs.getMessage().get());
+       }
+
+       // Helper method to create a mock converter for testing
+       private BeanConverter createMockConverter() {
+               return new BeanConverter() {
+                       @Override
+                       public String stringify(Object o) {
+                               return String.valueOf(o);
+                       }
+
+                       @Override
+                       public List<Object> listify(Object o) {
+                               if (o instanceof List) return (List<Object>) o;
+                               return List.of(o);
+                       }
+
+                       @Override
+                       public boolean canListify(Object o) {
+                               return true;
+                       }
+
+                       @Override
+                       public Object swap(Object o) {
+                               return o;
+                       }
+
+                       @Override
+                       public Object getProperty(Object object, String name) {
+                               // Simple mock implementation
+                               if ("name".equals(name) && object instanceof 
TestBean) {
+                                       return ((TestBean) object).getName();
+                               }
+                               if ("custom".equals(name) && object instanceof 
TestBeanWithCustomObject) {
+                                       return ((TestBeanWithCustomObject) 
object).getCustom();
+                               }
+                               return null;
+                       }
+
+                       @Override
+                       public <T> T getSetting(String key, T defaultValue) {
+                               return defaultValue;
+                       }
+
+                       @Override
+                       public String getNested(Object o, NestedTokenizer.Token 
token) {
+                               var propValue = getProperty(o, 
token.getValue());
+                               return stringify(propValue);
+                       }
+               };
+       }
+
+       // Helper method to create a custom converter for testing
+       private BeanConverter createCustomConverter() {
+               return new BeanConverter() {
+                       @Override
+                       public String stringify(Object o) {
+                               if (o instanceof CustomObject) {
+                                       return "CUSTOM:" + ((CustomObject) 
o).getValue();
+                               }
+                               return String.valueOf(o);
+                       }
+
+                       @Override
+                       public List<Object> listify(Object o) {
+                               if (o instanceof List) return (List<Object>) o;
+                               return List.of(o);
+                       }
+
+                       @Override
+                       public boolean canListify(Object o) {
+                               return true;
+                       }
+
+                       @Override
+                       public Object swap(Object o) {
+                               return o;
+                       }
+
+                       @Override
+                       public Object getProperty(Object object, String name) {
+                               if ("custom".equals(name) && object instanceof 
TestBeanWithCustomObject) {
+                                       return ((TestBeanWithCustomObject) 
object).getCustom();
+                               }
+                               return null;
+                       }
+
+                       @Override
+                       public <T> T getSetting(String key, T defaultValue) {
+                               return defaultValue;
+                       }
+
+                       @Override
+                       public String getNested(Object o, NestedTokenizer.Token 
token) {
+                               var propValue = getProperty(o, 
token.getValue());
+                               return stringify(propValue);
+                       }
+               };
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/junit/Assertions2.java 
b/juneau-utest/src/test/java/org/apache/juneau/junit/Assertions2.java
new file mode 100644
index 000000000..f2ba32295
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/Assertions2.java
@@ -0,0 +1,696 @@
+package org.apache.juneau.junit;
+
+import static java.util.stream.Collectors.*;
+import static org.apache.juneau.junit.Utils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import org.opentest4j.*;
+
+/**
+* Comprehensive utility class for Bean-Centric Tests (BCT) and general testing 
operations.
+*
+* <p>This class provides the core testing infrastructure for Apache Juneau, 
with particular emphasis
+* on the Bean-Centric Testing (BCT) framework. BCT enables sophisticated 
assertion patterns for
+* testing object properties, collections, maps, and complex nested structures 
with minimal code.</p>
+*
+* <h5 class='section'>Bean-Centric Testing (BCT) Framework:</h5>
+* <p>The BCT framework consists of several key components:</p>
+* <ul>
+*      <li><b>{@link BeanConverter}:</b> Core interface for object conversion 
and property access</li>
+*      <li><b>{@link BasicBeanConverter}:</b> Default implementation with 
extensible type handlers</li>
+*      <li><b>Assertion Methods:</b> High-level testing methods that leverage 
the converter framework</li>
+* </ul>
+*
+* <h5 class='section'>Primary BCT Assertion Methods:</h5>
+* <dl>
+*      <dt><b>{@link #assertBean(Object, String, String)}</b></dt>
+*      <dd>Tests object properties with nested syntax support and collection 
iteration</dd>
+*
+*      <dt><b>{@link #assertMap(Map, String, String)}</b></dt>
+*      <dd>Tests map entries with the same nested property syntax as 
assertBean</dd>
+*
+*      <dt><b>{@link #assertMapped(Object, java.util.function.BiFunction, 
String, String)}</b></dt>
+*      <dd>Tests custom property access using BiFunction for non-standard 
objects</dd>
+*
+*      <dt><b>{@link #assertList(List, Object...)}</b></dt>
+*      <dd>Tests list/collection elements with varargs for expected values</dd>
+*
+*      <dt><b>{@link #assertBeans(Collection, String, String...)}</b></dt>
+*      <dd>Tests collections of objects by extracting and comparing specific 
fields</dd>
+* </dl>
+*
+* <h5 class='section'>BCT Advanced Features:</h5>
+* <ul>
+*      <li><b>Nested Property Syntax:</b> "address{street,city}" for testing 
nested objects</li>
+*      <li><b>Collection Iteration:</b> "#{property}" syntax for testing all 
elements</li>
+*      <li><b>Universal Size Properties:</b> "length" and "size" work on all 
collection types</li>
+*      <li><b>Array/List Access:</b> Numeric indices for element-specific 
testing</li>
+*      <li><b>Method Chaining:</b> Fluent setters can be tested directly</li>
+*      <li><b>Direct Field Access:</b> Public fields accessed without 
getters</li>
+*      <li><b>Map Key Access:</b> Including special "&lt;NULL&gt;" syntax for 
null keys</li>
+* </ul>
+*
+* <h5 class='section'>Converter Extensibility:</h5>
+* <p>The BCT framework is built on the extensible {@link BasicBeanConverter} 
which allows:</p>
+* <ul>
+*      <li><b>Custom Stringifiers:</b> Type-specific string conversion 
logic</li>
+*      <li><b>Custom Listifiers:</b> Collection-type conversion for 
iteration</li>
+*      <li><b>Custom Swapifiers:</b> Object transformation before 
conversion</li>
+*      <li><b>Configurable Settings:</b> Formatting, delimiters, and display 
options</li>
+* </ul>
+*
+* <h5 class='section'>Usage Examples:</h5>
+*
+* <p><b>Basic Property Testing:</b></p>
+* <p class='bjava'>
+*      <jc>// Test multiple properties</jc>
+*      assertBean(user, <js>"name,age,active"</js>, <js>"John,30,true"</js>);
+*
+*      <jc>// Test nested properties</jc>
+*      assertBean(user, <js>"address{street,city}"</js>, <js>"{123 Main 
St,Springfield}"</js>);
+* </p>
+*
+* <p><b>Collection and Array Testing:</b></p>
+* <p class='bjava'>
+*      <jc>// Test collection size and iterate over all elements</jc>
+*      assertBean(order, <js>"items{length,#{name}}"</js>, 
<js>"{3,[{Laptop},{Phone},{Tablet}]}"</js>);
+*
+*      <jc>// Test specific array elements</jc>
+*      assertBean(data, <js>"values{0,1,2}"</js>, <js>"{100,200,300}"</js>);
+* </p>
+*
+* <p><b>Map and Collection Testing:</b></p>
+* <p class='bjava'>
+*      <jc>// Test map entries</jc>
+*      assertMap(config, <js>"timeout,retries"</js>, <js>"30000,3"</js>);
+*
+*      <jc>// Test list elements</jc>
+*      assertList(tags, <js>"red"</js>, <js>"green"</js>, <js>"blue"</js>);
+* </p>
+*
+* <p><b>Custom Property Access:</b></p>
+* <p class='bjava'>
+*      <jc>// Test with custom accessor function</jc>
+*      assertMapped(myObject, (obj, prop) -> obj.getProperty(prop),
+*              <js>"prop1,prop2"</js>, <js>"value1,value2"</js>);
+* </p>
+*
+* <h5 class='section'>Performance and Thread Safety:</h5>
+* <p>The BCT framework is designed for high performance with:</p>
+* <ul>
+*      <li><b>Caching:</b> Type-to-handler mappings cached for fast lookup</li>
+*      <li><b>Thread Safety:</b> All operations are thread-safe for concurrent 
testing</li>
+*      <li><b>Minimal Allocation:</b> Efficient object reuse and minimal 
temporary objects</li>
+* </ul>
+*
+* @see BeanConverter
+* @see BasicBeanConverter
+*/
+public class Assertions2 {
+
+       private static final BeanConverter DEFAULT_CONVERTER = 
BasicBeanConverter.DEFAULT;
+
+       public static AssertionArgs args() {
+               return new AssertionArgs();
+       }
+
+       /**
+        * Asserts that the fields/properties on the specified bean are the 
specified values after being converted to {@link Utils#r readable} strings.
+        *
+        * <p>This is the primary method for Bean-Centric Tests (BCT), 
supporting extensive property validation
+        * patterns including nested objects, collections, arrays, method 
chaining, direct field access, collection iteration
+        * with <js>#{property}</js> syntax, and universal 
<js>length</js>/<js>size</js> properties for all collection types.</p>
+        *
+        * <p>The method uses the {@link BasicBeanConverter#DEFAULT} converter 
internally for object introspection
+        * and value extraction. The converter provides sophisticated property 
access through the {@link BeanConverter}
+        * interface, supporting multiple fallback mechanisms for accessing 
object properties and values.</p>
+        *
+        * <h5 class='section'>Basic Usage:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test multiple properties</jc>
+        *      assertBean(myBean, <js>"prop1,prop2,prop3"</js>, 
<js>"val1,val2,val3"</js>);
+        *
+        *      <jc>// Test single property</jc>
+        *      assertBean(myBean, <js>"name"</js>, <js>"John"</js>);
+        * </p>
+        *
+        * <h5 class='section'>Nested Property Testing:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test nested bean properties</jc>
+        *      assertBean(myBean, <js>"address{street,city,state}"</js>, 
<js>"{123 Main St,Springfield,IL}"</js>);
+        *
+        *      <jc>// Test arbitrarily deep nesting</jc>
+        *      assertBean(myBean, <js>"person{address{geo{lat,lon}}}"</js>, 
<js>"{{{{40.7,-74.0}}}}"</js>);
+        * </p>
+        *
+         * <h5 class='section'>Array, List, and Stream Testing:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test array/list elements by index</jc>
+        *      assertBean(myBean, <js>"items{0,1,2}"</js>, 
<js>"{item1,item2,item3}"</js>);
+        *
+        *      <jc>// Test nested properties within array elements</jc>
+        *      assertBean(myBean, <js>"orders{0{id,total}}"</js>, 
<js>"{{123,99.95}}"</js>);
+        *
+        *      <jc>// Test array length property</jc>
+        *      assertBean(myBean, <js>"items{length}"</js>, <js>"{5}"</js>);
+        *
+        *      <jc>// Works with any iterable type including Streams</jc>
+        *      assertBean(myBean, <js>"userStream{#{name}}"</js>, 
<js>"[{Alice},{Bob}]"</js>);
+        * </p>
+        *
+        * <h5 class='section'>Collection Iteration Syntax:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test properties across ALL elements in a collection 
using #{...} syntax</jc>
+        *      assertBean(myBean, <js>"userList{#{name}}"</js>, 
<js>"[{John},{Jane},{Bob}]"</js>);
+        *
+        *      <jc>// Test multiple properties from each element</jc>
+        *      assertBean(myBean, <js>"orderList{#{id,status}}"</js>, 
<js>"[{123,ACTIVE},{124,PENDING}]"</js>);
+        *
+        *      <jc>// Works with nested properties within each element</jc>
+        *      assertBean(myBean, <js>"customers{#{address{city}}}"</js>, 
<js>"[{{New York}},{{Los Angeles}}]"</js>);
+        *
+         *     <jc>// Works with arrays and any iterable collection type 
(including Streams)</jc>
+        *      assertBean(config, <js>"itemArray{#{type}}"</js>, 
<js>"[{String},{Integer},{Boolean}]"</js>);
+        *      assertBean(data, <js>"statusSet{#{name}}"</js>, 
<js>"[{ACTIVE},{PENDING},{CANCELLED}]"</js>);
+        *      assertBean(processor, <js>"dataStream{#{value}}"</js>, 
<js>"[{A},{B},{C}]"</js>);
+        * </p>
+        *
+        * <h5 class='section'>Universal Collection Size Properties:</h5>
+        * <p class='bjava'>
+        *      <jc>// Both 'length' and 'size' work universally across all 
collection types</jc>
+        *      assertBean(myBean, <js>"myArray{length}"</js>, <js>"{5}"</js>); 
       <jc>// Arrays</jc>
+        *      assertBean(myBean, <js>"myArray{size}"</js>, <js>"{5}"</js>);   
       <jc>// Also works for arrays</jc>
+        *
+        *      assertBean(myBean, <js>"myList{size}"</js>, <js>"{3}"</js>);    
       <jc>// Collections</jc>
+        *      assertBean(myBean, <js>"myList{length}"</js>, <js>"{3}"</js>);  
       <jc>// Also works for collections</jc>
+        *
+        *      assertBean(myBean, <js>"myMap{size}"</js>, <js>"{7}"</js>);     
       <jc>// Maps</jc>
+        *      assertBean(myBean, <js>"myMap{length}"</js>, <js>"{7}"</js>);   
       <jc>// Also works for maps</jc>
+        * </p>
+        *
+        * <h5 class='section'>Class Name Testing:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test class properties (prefer simple names for 
maintainability)</jc>
+        *      assertBean(myBean, <js>"obj{class{simpleName}}"</js>, 
<js>"{{MyClass}}"</js>);
+        *
+        *      <jc>// Test full class names when needed</jc>
+        *      assertBean(myBean, <js>"obj{class{name}}"</js>, 
<js>"{{com.example.MyClass}}"</js>);
+        * </p>
+        *
+        * <h5 class='section'>Method Chaining Support:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test fluent setter chains (returns same object)</jc>
+        *      assertBean(
+        *              
item.setType(<js>"foo"</js>).setFormat(<js>"bar"</js>).setDefault(<js>"baz"</js>),
+        *              <js>"type,format,default"</js>,
+        *              <js>"foo,bar,baz"</js>
+        *      );
+        * </p>
+        *
+        * <h5 class='section'>Advanced Collection Analysis:</h5>
+        * <p class='bjava'>
+        *      <jc>// Combine size/length, metadata, and content iteration in 
single assertions</jc>
+        *      assertBean(myBean, 
<js>"users{length,class{simpleName},#{name}}"</js>,
+        *              <js>"{3,{ArrayList},[{John},{Jane},{Bob}]}"</js>);
+        *
+        *      <jc>// Comprehensive collection validation with multiple 
iteration patterns</jc>
+        *      assertBean(order, <js>"items{size,#{name},#{price}}"</js>,
+        *              
<js>"{3,[{Laptop},{Phone},{Tablet}],[{999.99},{599.99},{399.99}]}"</js>);
+        *
+        *      <jc>// Perfect for validation testing - verify error count and 
details</jc>
+        *      assertBean(result, <js>"errors{length,#{field},#{code}}"</js>,
+        *              <js>"{2,[{email},{password}],[{E001},{E002}]}"</js>);
+        *
+        *      <jc>// Mixed collection types with consistent syntax</jc>
+        *      assertBean(response, <js>"results{size},metadata{length}"</js>, 
<js>"{25},{4}"</js>);
+        * </p>
+        *
+        * <h5 class='section'>Direct Field Access:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test public fields directly (no getters required)</jc>
+        *      assertBean(myBean, <js>"f1,f2,f3"</js>, 
<js>"val1,val2,val3"</js>);
+        *
+        *      <jc>// Test field properties with chaining</jc>
+        *      assertBean(myBean, <js>"f1{length},f2{class{simpleName}}"</js>, 
<js>"{5},{{String}}"</js>);
+        * </p>
+        *
+         * <h5 class='section'>Map Testing:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test map values by key</jc>
+        *      assertBean(myBean, <js>"configMap{timeout,retries}"</js>, 
<js>"{30000,3}"</js>);
+        *
+        *      <jc>// Test map size</jc>
+        *      assertBean(myBean, <js>"settings{size}"</js>, <js>"{5}"</js>);
+        *
+        *      <jc>// Test null keys using special &lt;NULL&gt; syntax</jc>
+        *      assertBean(myBean, <js>"mapWithNullKey{&lt;NULL&gt;}"</js>, 
<js>"{nullKeyValue}"</js>);
+        * </p>
+        *
+        * <h5 class='section'>Collection and Boolean Values:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test boolean values</jc>
+        *      assertBean(myBean, <js>"enabled,visible"</js>, 
<js>"true,false"</js>);
+        *
+        *      <jc>// Test enum collections</jc>
+        *      assertBean(myBean, <js>"statuses"</js>, 
<js>"[ACTIVE,PENDING]"</js>);
+        * </p>
+        *
+        * <h5 class='section'>Value Syntax Rules:</h5>
+        * <ul>
+        *      <li><b>Simple values:</b> <js>"value"</js> for direct property 
values</li>
+        *      <li><b>Nested values:</b> <js>"{value}"</js> for single-level 
nested properties</li>
+        *      <li><b>Deep nested values:</b> <js>"{{value}}"</js>, 
<js>"{{{value}}}"</js> for multiple nesting levels</li>
+        *      <li><b>Array/Collection values:</b> <js>"[item1,item2]"</js> 
for collections</li>
+        *      <li><b>Collection iteration:</b> <js>"#{property}"</js> 
iterates over ALL collection elements, returns <js>"[{val1},{val2}]"</js></li>
+        *      <li><b>Universal size properties:</b> <js>"length"</js> and 
<js>"size"</js> work on arrays, collections, and maps</li>
+        *      <li><b>Boolean values:</b> <js>"true"</js>, 
<js>"false"</js></li>
+        *      <li><b>Null values:</b> <js>"null"</js></li>
+        * </ul>
+        *
+         * <h5 class='section'>Property Access Priority:</h5>
+        * <ol>
+        *      <li><b>Collection/Array access:</b> Numeric indices for 
arrays/lists (e.g., <js>"0"</js>, <js>"1"</js>)</li>
+        *      <li><b>Universal size properties:</b> <js>"length"</js> and 
<js>"size"</js> for arrays, collections, and maps</li>
+        *      <li><b>Map key access:</b> Direct key lookup for Map objects 
(including <js>"&lt;NULL&gt;"</js> for null keys)</li>
+        *      <li><b>is{Property}()</b> methods (for boolean properties)</li>
+        *      <li><b>get{Property}()</b> methods</li>
+        *      <li><b>Public fields</b> (direct field access)</li>
+        * </ol>
+        *
+        * @param actual The bean object to test. Must not be null.
+        * @param fields Comma-delimited list of property names to test. 
Supports nested syntax with {}.
+        * @param expected Comma-delimited list of expected values. Must match 
the order of fields.
+        * @throws NullPointerException if the bean is null
+        * @throws AssertionError if any property values don't match expected 
values
+        * @see BeanConverter
+        * @see BasicBeanConverter
+        */
+       public static void assertBean(Object actual, String fields, String 
expected) {
+               assertBean(args(), actual, fields, expected);
+       }
+
+       public static void assertBean(AssertionArgs args, Object actual, String 
fields, String expected) {
+               assertNotNull(actual, "Actual was null.");
+               assertArgNotNull("args", args);
+               assertArgNotNull("fields", fields);
+               assertArgNotNull("expected", expected);
+               assertEquals(
+                       expected,
+                       tokenize(fields).stream().map(x -> 
args.getBeanConverter().orElse(DEFAULT_CONVERTER).getNested(actual, 
x)).collect(joining(",")),
+                       args.getMessage("Bean assertion failed.")
+               );
+       }
+
+       /**
+        * Asserts that multiple beans in a collection have the expected 
property values.
+        *
+        * <p>This method validates that each bean in a collection has the 
specified property values,
+        * using the same property access logic as {@link #assertBean(Object, 
String, String)}.
+        * It's perfect for testing collections of similar objects or 
validation results.</p>
+        *
+        * <h5 class='section'>Basic Usage:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test list of user beans</jc>
+        *      assertBeans(userList, <js>"name,age"</js>,
+        *              <js>"John,25"</js>, <js>"Jane,30"</js>, 
<js>"Bob,35"</js>);
+        * </p>
+        *
+        * <h5 class='section'>Complex Property Testing:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test nested properties across multiple beans</jc>
+        *      assertBeans(orderList, <js>"id,customer{name,email}"</js>,
+        *              <js>"1,{John,j...@example.com}"</js>,
+        *              <js>"2,{Jane,j...@example.com}"</js>);
+        *
+        *      <jc>// Test collection properties within beans</jc>
+        *      assertBeans(cartList, <js>"items{0{name}},total"</js>,
+        *              <js>"{{Laptop}},999.99"</js>,
+        *              <js>"{{Phone}},599.99"</js>);
+        * </p>
+        *
+        * <h5 class='section'>Validation Testing:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test validation results</jc>
+        *      assertBeans(validationErrors, <js>"field,message,code"</js>,
+        *              <js>"email,Invalid email format,E001"</js>,
+        *              <js>"age,Must be 18 or older,E002"</js>);
+        * </p>
+        *
+         * <h5 class='section'>Collection Iteration Testing:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test collection iteration within beans (#{...} 
syntax)</jc>
+        *      assertBeans(departmentList, <js>"name,employees{#{name}}"</js>,
+        *              <js>"Engineering,[{Alice},{Bob},{Charlie}]"</js>,
+        *              <js>"Marketing,[{David},{Eve}]"</js>);
+        * </p>
+        *
+        * <h5 class='section'>Parser Result Testing:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test parsed object collections</jc>
+        *      var parsed = JsonParser.DEFAULT.parse(jsonArray, 
MyBean[].class);
+        *      assertBeans(Arrays.asList(parsed), <js>"prop1,prop2"</js>,
+        *              <js>"val1,val2"</js>, <js>"val3,val4"</js>);
+        * </p>
+        *
+        * @param listOfBeans The collection of beans to check. Must not be 
null.
+        * @param fields A comma-delimited list of bean property names 
(supports nested syntax).
+        * @param values Array of expected value strings, one per bean. Each 
string contains comma-delimited values matching the fields.
+        * @throws AssertionError if the collection size doesn't match values 
array length or if any bean properties don't match
+        * @see #assertBean(Object, String, String)
+        */
+       public static void assertBeans(Object actual, String fields, 
String...expected) {
+               assertBeans(args(), actual, fields, expected);
+       }
+
+       public static void assertBeans(AssertionArgs args, Object actual, 
String fields, String...expected) {
+               assertNotNull(actual, "Value was null.");
+               assertArgNotNull("args", args);
+               assertArgNotNull("fields", fields);
+               assertArgNotNull("expected", expected);
+
+               var converter = 
args.getBeanConverter().orElse(DEFAULT_CONVERTER);
+               var tokens = tokenize(fields);
+               var errors = new ArrayList<AssertionFailedError>();
+               var actualList = converter.listify(actual);
+
+               if (ne(expected.length, actualList.size())) {
+                       errors.add(assertEqualsFailed(expected.length, 
actualList.size(), args.getMessage("Wrong number of beans.")));
+               } else {
+                       for (var i = 0; i < actualList.size(); i++) {
+                               var i2 = i;
+                               var e = converter.stringify(expected[i]);
+                               var a = tokens.stream().map(x -> 
converter.getNested(actualList.get(i2), x)).collect(joining(","));
+                               if (ne(e, a)) {
+                                       errors.add(assertEqualsFailed(e, a, 
args.getMessage("Bean at row <{0}> did not match.", i)));
+                               }
+                       }
+               }
+
+               if (errors.isEmpty()) return;
+
+               var actualStrings = new ArrayList<String>();
+               for (var o : actualList) {
+                       actualStrings.add(tokens.stream().map(x -> 
converter.getNested(o, x)).collect(joining(",")));
+               }
+
+               if (errors.size() == 1) throw errors.get(0);
+
+               throw assertEqualsFailed(
+                       
Stream.of(expected).map(Utils::escapeForJava).collect(joining("\", \"", "\"", 
"\"")),
+                       
actualStrings.stream().map(Utils::escapeForJava).collect(joining("\", \"", 
"\"", "\"")),
+                       args.getMessage("{0} bean assertions failed:\n{1}", 
errors.size(), errors.stream().map(x -> x.getMessage()).collect(joining("\n")))
+               );
+       }
+
+       /**
+        * Asserts that mapped property access on an object returns expected 
values using a custom BiFunction.
+        *
+        * <p>This is the most powerful and flexible BCT method, designed for 
testing objects that don't follow
+        * standard JavaBean patterns or require custom property access logic. 
The BiFunction allows complete
+        * control over how properties are retrieved from the target object.</p>
+        *
+        * <p>When the BiFunction throws an exception, it's automatically 
caught and the exception's
+        * simple class name becomes the property value for comparison (e.g., 
"NullPointerException").</p>
+        *
+        * <p>This method creates an intermediate LinkedHashMap to collect all 
property values before
+        * delegating to assertMap(Map, String, String). This ensures 
consistent ordering
+        * and supports the full nested property syntax. The {@link 
BasicBeanConverter#DEFAULT} is used
+        * for value stringification and nested property access.</p>
+        *
+        * @param <T> The type of object being tested
+        * @param actual The object to test properties on
+        * @param function The BiFunction that extracts property values. 
Receives (object, propertyName) and returns the property value.
+        * @param properties Comma-delimited list of property names to test
+        * @param expected Comma-delimited list of expected values (exceptions 
become simple class names)
+        * @throws AssertionError if any mapped property values don't match 
expected values
+        * @see #assertBean(Object, String, String)
+        * @see #assertMap(Map, String, String)
+        * @see BeanConverter
+        * @see BasicBeanConverter
+        */
+       public static <T> void assertMapped(T actual, 
BiFunction<T,String,Object> function, String properties, String expected) {
+               assertMapped(args(), actual, function, properties, expected);
+       }
+
+       public static <T> void assertMapped(AssertionArgs args, T actual, 
BiFunction<T,String,Object> function, String properties, String expected) {
+               assertNotNull(actual, "Value was null.");
+               assertArgNotNull("args", args);
+               assertArgNotNull("function", function);
+               assertArgNotNull("properties", properties);
+               assertArgNotNull("expected", expected);
+
+               var m = new LinkedHashMap<String,Object>();
+               for (var p : tokenize(properties)) {
+                       var pv = p.getValue();
+                       m.put(pv, safe(() -> function.apply(actual, pv)));
+               }
+
+               assertBean(args, m, properties, expected);
+       }
+
+       /**
+        * Asserts an object matches the expected string after it's been made 
{@link Utils#r readable}.
+        */
+       public static void assertContains(String expected, Object actual) {
+               assertContains(args(), expected, actual);
+       }
+
+       public static void assertContains(AssertionArgs args, String expected, 
Object actual) {
+               assertArgNotNull("args", args);
+               assertArgNotNull("expected", expected);
+               assertArgNotNull("actual", actual);
+               assertNotNull(actual, "Value was null.");
+
+               var a = 
args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(actual);
+               assertTrue(a.contains(expected), args.getMessage("String did 
not contain expected substring.  ==> expected: <{0}> but was: <{1}>", expected, 
a));
+       }
+
+       /**
+        * Similar to {@link #assertContains(String, Object)} but allows the 
expected to be a comma-delimited list of strings that
+        * all must match.
+        * @param expected
+        * @param actual
+        */
+       public static void assertContainsAll(Object actual, String...expected) {
+               assertContainsAll(args(), actual, expected);
+       }
+
+       public static void assertContainsAll(AssertionArgs args, Object actual, 
String...expected) {
+               assertArgNotNull("args", args);
+               assertArgNotNull("expected", expected);
+               assertNotNull(actual, "Value was null.");
+
+               var a = 
args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(actual);
+               for (var e : expected)
+                       assertTrue(a.contains(e), args.getMessage("String did 
not contain expected substring.  ==> expected: <{0}> but was: <{1}>", e, a));
+       }
+
+       /**
+        * Asserts that a collection is not null and empty.
+        */
+       public static void assertEmpty(Object value) {
+               assertEmpty(args(), value);
+       }
+
+       public static void assertEmpty(AssertionArgs args, Object value) {
+               assertArgNotNull("args", args);
+               assertNotNull(value, "Value was null.");
+
+               if (value instanceof Optional v2) {
+                       assertTrue(v2.isEmpty(), "Optional was not empty");
+                       return;
+               }
+               if (value instanceof Map v2) {
+                       assertTrue(v2.isEmpty(), "Map was not empty");
+                       return;
+               }
+
+               var converter = 
args.getBeanConverter().orElse(DEFAULT_CONVERTER);
+
+               assertTrue(converter.canListify(value), args.getMessage("Value 
cannot be converted to a list.  Class=<{0}>", 
value.getClass().getSimpleName()));
+               assertTrue(converter.listify(value).isEmpty(), 
args.getMessage("Value was not empty."));
+       }
+
+       /**
+        * Asserts that a List contains the expected values using flexible 
comparison logic.
+        *
+        * <p>This is the primary method for testing all collection-like types. 
For non-List collections, use
+        * {@link #l(Object)} to convert them to Lists first. This unified 
approach eliminates the need for
+        * separate assertion methods for arrays, sets, and other collection 
types.</p>
+        *
+        * <h5 class='section'>Testing Non-List Collections:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test a Set using l() conversion</jc>
+        *      Set&lt;String&gt; <jv>mySet</jv> = Set.of(<js>"a"</js>, 
<js>"b"</js>, <js>"c"</js>);
+        *      assertList(l(<jv>mySet</jv>), <js>"a"</js>, <js>"b"</js>, 
<js>"c"</js>);
+        *
+        *      <jc>// Test an array using l() conversion</jc>
+        *      String[] <jv>myArray</jv> = {<js>"x"</js>, <js>"y"</js>, 
<js>"z"</js>};
+        *      assertList(l(<jv>myArray</jv>), <js>"x"</js>, <js>"y"</js>, 
<js>"z"</js>);
+        *
+        *      <jc>// Test a Stream using l() conversion</jc>
+        *      Stream&lt;String&gt; <jv>myStream</jv> = 
Stream.of(<js>"foo"</js>, <js>"bar"</js>);
+        *      assertList(l(<jv>myStream</jv>), <js>"foo"</js>, 
<js>"bar"</js>);
+        * </p>
+        *
+        * <h5 class='section'>Comparison Modes:</h5>
+        * <p>The method supports three different ways to compare expected vs 
actual values:</p>
+        *
+        * <h6 class='section'>1. String Comparison (Readable Format):</h6>
+        * <p class='bjava'>
+        *      <jc>// Elements are converted to {@link Utils#r readable} 
format and compared as strings</jc>
+        *      assertList(List.of(1, 2, 3), <js>"1"</js>, <js>"2"</js>, 
<js>"3"</js>);
+        *      assertList(List.of("a", "b"), <js>"a"</js>, <js>"b"</js>);
+        * </p>
+        *
+        * <h6 class='section'>2. Predicate Testing (Functional 
Validation):</h6>
+        * <p class='bjava'>
+        *      <jc>// Use Predicate&lt;T&gt; for functional testing</jc>
+        *      Predicate&lt;Integer&gt; <jv>greaterThanOne</jv> = <jv>x</jv> 
-&gt; <jv>x</jv> &gt; 1;
+        *      assertList(List.of(2, 3, 4), <jv>greaterThanOne</jv>, 
<jv>greaterThanOne</jv>, <jv>greaterThanOne</jv>);
+        *
+        *      <jc>// Mix predicates with other comparison types</jc>
+        *      Predicate&lt;String&gt; <jv>startsWithA</jv> = <jv>s</jv> -&gt; 
<jv>s</jv>.startsWith(<js>"a"</js>);
+        *      assertList(List.of(<js>"apple"</js>, <js>"banana"</js>), 
<jv>startsWithA</jv>, <js>"banana"</js>);
+        * </p>
+        *
+        * <h6 class='section'>3. Object Equality (Direct Comparison):</h6>
+        * <p class='bjava'>
+        *      <jc>// Non-String, non-Predicate objects use Objects.equals() 
comparison</jc>
+        *      assertList(List.of(1, 2, 3), 1, 2, 3); <jc>// Integer 
objects</jc>
+        *      assertList(List.of(<jv>myBean1</jv>, <jv>myBean2</jv>), 
<jv>myBean1</jv>, <jv>myBean2</jv>); <jc>// Custom objects</jc>
+        * </p>
+        *
+        * @param actual The List to test. Must not be null. For other 
collection types, use {@link #l(Object)} to convert first.
+        * @param expected Multiple arguments of expected values.
+        *                 Can be Strings (readable format comparison), 
Predicates (functional testing), or Objects (direct equality).
+        * @throws AssertionError if the List size or contents don't match 
expected values
+        * @see #l(Object) for converting other collection types to Lists
+        */
+       public static <T> void assertList(Object actual, Object...expected) {
+               assertList(args(), actual, expected);
+       }
+
+       @SuppressWarnings("unchecked")
+       public static <T> void assertList(AssertionArgs args, Object actual, 
Object...expected) {
+               assertArgNotNull("args", args);
+               assertArgNotNull("expected", expected);
+               assertNotNull(actual, "Value was null.");
+
+               var converter = 
args.getBeanConverter().orElse(DEFAULT_CONVERTER);
+               var list = converter.listify(actual);
+               assertEquals(expected.length, list.size(), 
args.getMessage("Wrong list length."));
+
+               for (var i = 0; i < expected.length; i++) {
+                       var x = list.get(i);
+                       var e = expected[i];
+                       if (e instanceof String e2) {
+                               assertEquals(e2, converter.stringify(x), 
args.getMessage("Element at index {0} did not match.", i));
+                       } else if (e instanceof Predicate e2) {
+                               assertTrue(e2.test(x), args.getMessage("Element 
at index {0} did pass predicate.  ==> actual: <{0}>", i, 
converter.stringify(x)));
+                       } else {
+                               assertEquals(e, x, args.getMessage("Element at 
index {0} did not match.  ==> expected: <{1}({2})> but was: <{3}(4)>", i, e, 
t(e), x, t(x)));
+                       }
+               }
+       }
+
+       /**
+        * Asserts that a collection is not null and not empty.
+        */
+       public static void assertNotEmpty(Object value) {
+               assertNotEmpty(args(), value);
+       }
+
+       public static void assertNotEmpty(AssertionArgs args, Object value) {
+               assertArgNotNull("args", args);
+               assertNotNull(value, "Value was null.");
+
+               if (value instanceof Optional v2) {
+                       assertFalse(v2.isEmpty(), "Optional was empty");
+                       return;
+               }
+               if (value instanceof Map v2) {
+                       assertFalse(v2.isEmpty(), "Map was empty");
+                       return;
+               }
+
+               
assertFalse(args.getBeanConverter().orElse(DEFAULT_CONVERTER).listify(value).isEmpty(),
 args.getMessage("Value was empty."));
+       }
+
+       /**
+        * Asserts that a collection-like object or string is not null and of 
the specified size.
+        *
+        * <p>This method can validate the size of various types of objects:</p>
+        * <ul>
+        *      <li><b>String:</b> Validates character length</li>
+        *      <li><b>Collection-like objects:</b> Any object that can be 
converted to a List via {@link #toList(Object)}</li>
+        * </ul>
+        *
+        * <h5 class='section'>Usage Examples:</h5>
+        * <p class='bjava'>
+        *      <jc>// Test string length</jc>
+        *      assertSize(5, <js>"hello"</js>);
+        *
+        *      <jc>// Test collection size</jc>
+        *      assertSize(3, List.of(<js>"a"</js>, <js>"b"</js>, 
<js>"c"</js>));
+        *
+        *      <jc>// Test array size</jc>
+        *      assertSize(2, <jk>new</jk> String[]{<js>"x"</js>, 
<js>"y"</js>});
+        * </p>
+        *
+        * @param expected The expected size/length.
+        * @param actual The object to test. Must not be null.
+        * @throws AssertionError if the object is null or not the expected 
size.
+        */
+       public static void assertSize(int expected, Object actual) {
+               assertSize(args(), expected, actual);
+       }
+
+       public static void assertSize(AssertionArgs args, int expected, Object 
actual) {
+               assertArgNotNull("args", args);
+               assertNotNull(actual, "Value was null.");
+
+               if (actual instanceof String a) {
+                       assertEquals(expected, a.length(), 
args.getMessage("Value not expected size.  value: <{0}>", a));
+                       return;
+               }
+
+               var size = 
args.getBeanConverter().orElse(DEFAULT_CONVERTER).listify(actual).size();
+               assertEquals(expected, size, args.getMessage("Value not 
expected size."));
+       }
+
+       /**
+        * Asserts an object matches the expected string after it's been made 
{@link Utils#r readable}.
+        */
+       public static void assertString(String expected, Object actual) {
+               assertString(args(), expected, actual);
+       }
+
+       public static void assertString(AssertionArgs args, String expected, 
Object actual) {
+               assertArgNotNull("args", args);
+               assertNotNull(actual, "Value was null.");
+
+               assertEquals(expected, 
args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(actual), 
args.getMessage());
+       }
+
+       /**
+        * Asserts value when stringified matches the specified pattern.
+        */
+       public static void assertMatches(String pattern, Object value) {
+               assertMatches(args(), pattern, value);
+       }
+
+       public static void assertMatches(AssertionArgs args, String pattern, 
Object value) {
+               assertArgNotNull("args", args);
+               assertArgNotNull("pattern", pattern);
+               assertNotNull(value, "Value was null.");
+
+               var v = 
args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(value);
+               var m = getMatchPattern3(pattern).matcher(v);
+               assertTrue(m.matches(), args.getMessage("Pattern didn't match. 
==> pattern: <{0}> but was: <{1}>", pattern, v));
+       }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/junit/Utils.java 
b/juneau-utest/src/test/java/org/apache/juneau/junit/Utils.java
index 9b29b735c..c1e057456 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/junit/Utils.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/Utils.java
@@ -12,10 +12,15 @@
 // 
***************************************************************************************************************************
 package org.apache.juneau.junit;
 
+import static java.util.Optional.*;
+
 import java.lang.reflect.*;
 import java.text.*;
 import java.util.*;
 import java.util.function.*;
+import java.util.regex.*;
+
+import org.opentest4j.*;
 
 public class Utils {
        public static <T> T safe(ThrowingSupplier<T> s) {
@@ -32,6 +37,14 @@ public class Utils {
                return args.length == 0 ? msg : MessageFormat.format(msg, args);
        }
 
+       public static Supplier<String> fs(String msg, Object...args) {
+               return ()->f(msg, args);
+       }
+
+       public static String t(Object o) {
+               return o == null ? null : o.getClass().getSimpleName();
+       }
+
        public static List<Object> arrayToList(Object o) {
                var l = new ArrayList<>();
                for (var i = 0; i < Array.getLength(o); i++)
@@ -46,8 +59,105 @@ public class Utils {
                return test.test(o1, o2);
        }
 
-       @SuppressWarnings("unlikely-arg-type")
-       public static <T,U> boolean eq(T o1, U o2) {
+       public static <T> boolean eq(T o1, T o2) {
                return Objects.equals(o1, o2);
        }
+
+       public static <T> boolean ne(T o1, T o2) {
+               return ! eq(o1, o2);
+       }
+
+       /**
+        * Throws an {@link IllegalArgumentException} if the specified argument 
is <jk>null</jk>.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      <jk>import static</jk> org.apache.juneau.internal.ArgUtils.*;
+        *
+        *      <jk>public</jk> String setFoo(String <jv>foo</jv>) {
+        *              <jsm>assertArgNotNull</jsm>(<js>"foo"</js>, 
<jv>foo</jv>);
+        *              ...
+        *      }
+        * </p>
+        *
+        * @param <T> The argument data type.
+        * @param name The argument name.
+        * @param o The object to check.
+        * @return The same argument.
+        * @throws IllegalArgumentException Constructed exception.
+        */
+       public static final <T> T assertArgNotNull(String name, T o) throws 
IllegalArgumentException {
+               assertArg(o != null, "Argument ''{0}'' cannot be null.", name);
+               return o;
+       }
+
+       public static final void assertArg(boolean expression, String msg, 
Object...args) throws IllegalArgumentException {
+               if (! expression)
+                       throw new 
IllegalArgumentException(MessageFormat.format(msg, args));
+       }
+
+       /**
+        * Converts a string containing <js>"*"</js> meta characters with a 
regular expression pattern.
+        *
+        * @param s The string to create a pattern from.
+        * @return A regular expression pattern.
+        */
+       public static Pattern getMatchPattern3(String s) {
+               return getMatchPattern3(s, 0);
+       }
+
+       /**
+        * Converts a string containing <js>"*"</js> meta characters with a 
regular expression pattern.
+        *
+        * @param s The string to create a pattern from.
+        * @param flags Regular expression flags.
+        * @return A regular expression pattern.
+        */
+       public static Pattern getMatchPattern3(String s, int flags) {
+               if (s == null)
+                       return null;
+               var sb = new StringBuilder();
+               sb.append("\\Q");
+               for (var i = 0; i < s.length(); i++) {
+                       var c = s.charAt(i);
+                       if (c == '*')
+                               sb.append("\\E").append(".*").append("\\Q");
+                       else if (c == '?')
+                               sb.append("\\E").append(".").append("\\Q");
+                       else
+                               sb.append(c);
+               }
+               sb.append("\\E");
+               return Pattern.compile(sb.toString(), flags);
+       }
+
+       public static List<NestedTokenizer.Token> tokenize(String fields) {
+               return NestedTokenizer.tokenize(fields);
+       }
+
+       public static AssertionFailedError assertEqualsFailed(Object expected, 
Object actual, Supplier<String> messageSupplier) {
+               return new 
AssertionFailedError(ofNullable(messageSupplier).map(x -> 
x.get()).orElse("Equals assertion failed.") + f(" ==> expected: <{0}> but was: 
<{1}>", expected, actual), expected, actual);
+       }
+
+       public static String escapeForJava(String s) {
+               var sb = new StringBuilder();
+               for (var c : s.toCharArray()) {
+                       switch (c) {
+                               case '\"': sb.append("\\\""); break;
+                               case '\\': sb.append("\\\\"); break;
+                               case '\n': sb.append("\\n"); break;
+                               case '\r': sb.append("\\r"); break;
+                               case '\t': sb.append("\\t"); break;
+                               case '\f': sb.append("\\f"); break;
+                               case '\b': sb.append("\\b"); break;
+                               default:
+                                       if (c < 0x20 || c > 0x7E) {
+                                               
sb.append(String.format("\\u%04x", (int)c));
+                                       } else {
+                                               sb.append(c);
+                                       }
+                       }
+               }
+               return sb.toString();
+       }
 }
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/objecttools/ObjectRest_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/objecttools/ObjectRest_Test.java
index cbd4dc43f..c04054797 100755
--- 
a/juneau-utest/src/test/java/org/apache/juneau/objecttools/ObjectRest_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/objecttools/ObjectRest_Test.java
@@ -16,6 +16,7 @@ import static org.apache.juneau.TestUtils.*;
 import static org.junit.jupiter.api.Assertions.*;
 
 import java.util.*;
+import java.util.function.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.annotation.*;
@@ -362,7 +363,15 @@ class ObjectRest_Test extends SimpleTestBase {
                        "f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
                        
"true,false,false,false,true,true,true,true,true,true,true");
 
-               assertMapped(model, ObjectRest::getMap,
+               BiFunction<ObjectRest,String,Object> f1 = (r,p) -> {
+                       try {
+                               return r.getMap(p);
+                       } catch (Exception e) {
+                               return e.getClass().getSimpleName();
+                       }
+               };
+
+               assertMapped(model, f1,
                        "f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
                        
"<null>,InvalidDataConversionException,InvalidDataConversionException,InvalidDataConversionException,<null>,<null>,<null>,<null>,<null>,<null>,<null>");
 
@@ -378,15 +387,39 @@ class ObjectRest_Test extends SimpleTestBase {
                assertEquals("{a:'b'}", model.getMap("f7", m).toString());
                assertEquals("{a:'b'}", model.getMap("f8", m).toString());
 
-               assertMapped(model, (r,p) -> r.getMap(p, m),
+               BiFunction<ObjectRest,String,Object> f2 = (r,p) -> {
+                       try {
+                               return r.getMap(p, m);
+                       } catch (Exception e) {
+                               return e.getClass().getSimpleName();
+                       }
+               };
+
+               assertMapped(model, f2,
                        "f1,f2,f2a,f3,f3a,f4,f4a,f5,f6,f7,f8",
                        
"{a=b},InvalidDataConversionException,{a=b},InvalidDataConversionException,{a=b},InvalidDataConversionException,{a=b},{a=b},{a=b},{a=b},{a=b}");
 
-               assertMapped(model, ObjectRest::getJsonMap,
+               BiFunction<ObjectRest,String,Object> f3 = (r,p) -> {
+                       try {
+                               return r.getJsonMap(p);
+                       } catch (Exception e) {
+                               return e.getClass().getSimpleName();
+                       }
+               };
+
+               assertMapped(model, f3,
                        "f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
                        
"<null>,InvalidDataConversionException,InvalidDataConversionException,InvalidDataConversionException,<null>,<null>,<null>,<null>,<null>,<null>,<null>");
 
-               assertMapped(model, (r,p) -> r.getJsonMap(p, m),
+               BiFunction<ObjectRest,String,Object> f4 = (r,p) -> {
+                       try {
+                               return r.getJsonMap(p, m);
+                       } catch (Exception e) {
+                               return e.getClass().getSimpleName();
+                       }
+               };
+
+               assertMapped(model, f4,
                        "f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
                        
"{a=b},InvalidDataConversionException,InvalidDataConversionException,InvalidDataConversionException,{a=b},{a=b},{a=b},{a=b},{a=b},{a=b},{a=b}");
 


Reply via email to