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 94175423d6 juneau-bct improvements
94175423d6 is described below

commit 94175423d67b94361672faf5f81053abb2cadcdd
Author: James Bognar <[email protected]>
AuthorDate: Fri Dec 19 12:25:17 2025 -0500

    juneau-bct improvements
---
 .../org/apache/juneau/bean/openapi3/MediaType.java |   4 +-
 .../org/apache/juneau/bean/openapi3/Parameter.java |   2 +-
 .../org/apache/juneau/bean/openapi3/PathItem.java  |   2 +-
 .../apache/juneau/bean/openapi3/SchemaInfo.java    |   4 +-
 .../org/apache/juneau/bean/swagger/HeaderInfo.java |   6 +-
 .../java/org/apache/juneau/bean/swagger/Items.java |   6 +-
 .../apache/juneau/bean/swagger/ParameterInfo.java  |   4 +-
 .../org/apache/juneau/bean/swagger/SchemaInfo.java |   6 +-
 juneau-core/juneau-bct/pom.xml                     |   3 +-
 .../org/apache/juneau/junit/bct/BctAssertions.java | 197 ++++--------
 .../apache/juneau/junit/bct/BctConfiguration.java  | 309 ++++++++++++++++++
 .../org/apache/juneau/junit/bct/Listifiers.java    |   6 +-
 .../juneau/junit/bct/annotations/BctConfig.java    |  90 ++++++
 .../junit/bct/annotations/BctConfigExtension.java  |  76 +++++
 .../org/apache/juneau/commons/lang/TriState.java   |  77 +++++
 .../docs/topics/07.01.00.JuneauBctBasics.md        |  48 ++-
 .../docs/topics/07.01.04.PropertyExtractors.md     |   7 +-
 ...Messages.md => 07.02.00.CustomErrorMessages.md} |   9 +-
 juneau-docs/docs/topics/07.03.00.Customization.md  | 351 +++++++++++++++++++++
 ...01.Stringifiers.md => 07.03.01.Stringifiers.md} |   7 +-
 ....01.02.Listifiers.md => 07.03.02.Listifiers.md} |   7 +-
 .../{07.01.03.Swappers.md => 07.03.03.Swappers.md} |   7 +-
 juneau-docs/sidebars.ts                            |  42 ++-
 .../java/org/apache/juneau/BeanSession_Test.java   |   2 -
 .../juneau/bean/openapi3/HeaderInfo_Test.java      |   2 +-
 .../apache/juneau/bean/openapi3/Items_Test.java    |   2 +-
 .../juneau/bean/openapi3/Parameter_Test.java       |   2 +-
 .../juneau/bean/openapi3/SchemaInfo_Test.java      |   2 +-
 .../juneau/bean/swagger/SchemaInfo_Test.java       |   2 +-
 .../juneau/commons/collections/Maps_Test.java      |   3 +
 .../juneau/commons/collections/Sets_Test.java      |   3 +
 .../juneau/junit/bct/BasicBeanConverter_Test.java  |   4 +
 .../juneau/junit/bct/BctAssertions_Test.java       |   3 +
 .../apache/juneau/junit/bct/BctConfig_Test.java    | 306 ++++++++++++++++++
 .../apache/juneau/junit/bct/Listifiers_Test.java   |   9 +-
 35 files changed, 1398 insertions(+), 212 deletions(-)

diff --git 
a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/MediaType.java
 
b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/MediaType.java
index 603524e5a7..e5b8d6c18a 100644
--- 
a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/MediaType.java
+++ 
b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/MediaType.java
@@ -182,10 +182,10 @@ public class MediaType extends OpenApiElement {
        public Set<String> keySet() {
                // @formatter:off
                var s = setb(String.class)
-                       .addIf(nn(schema), "schema")
-                       .addIf(nn(example), "x-example")
                        .addIf(isNotEmpty(encoding), "encoding")
                        .addIf(isNotEmpty(examples), "examples")
+                       .addIf(nn(schema), "schema")
+                       .addIf(nn(example), "x-example")
                        .build();
                // @formatter:on
                return new MultiSet<>(s, super.keySet());
diff --git 
a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/Parameter.java
 
b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/Parameter.java
index e19989ae90..f932b39284 100644
--- 
a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/Parameter.java
+++ 
b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/Parameter.java
@@ -234,8 +234,8 @@ public class Parameter extends OpenApiElement {
                var s = setb(String.class)
                        .addIf(nn(allowEmptyValue), "allowEmptyValue")
                        .addIf(nn(allowReserved), "allowReserved")
-                       .addIf(nn(description), "description")
                        .addIf(nn(deprecated), "deprecated")
+                       .addIf(nn(description), "description")
                        .addIf(nn(example), "example")
                        .addIf(nn(examples), "examples")
                        .addIf(nn(explode), "explode")
diff --git 
a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/PathItem.java
 
b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/PathItem.java
index 2cbefe48a3..c6f97e0465 100644
--- 
a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/PathItem.java
+++ 
b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/PathItem.java
@@ -235,8 +235,8 @@ public class PathItem extends OpenApiElement {
                        .addIf(nn(get), "get")
                        .addIf(nn(head), "head")
                        .addIf(nn(options), "options")
-                       .addIf(nn(patch), "patch")
                        .addIf(nn(parameters), "parameters")
+                       .addIf(nn(patch), "patch")
                        .addIf(nn(post), "post")
                        .addIf(nn(put), "put")
                        .addIf(nn(servers), "servers")
diff --git 
a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/SchemaInfo.java
 
b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/SchemaInfo.java
index 0d8348eb66..fc87313b4f 100644
--- 
a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/SchemaInfo.java
+++ 
b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/SchemaInfo.java
@@ -654,14 +654,14 @@ public class SchemaInfo extends OpenApiElement {
                        .addIf(nn(externalDocs), "externalDocs")
                        .addIf(nn(format), "format")
                        .addIf(nn(items), "items")
-                       .addIf(nn(maximum), "maximum")
                        .addIf(nn(maxItems), "maxItems")
                        .addIf(nn(maxLength), "maxLength")
                        .addIf(nn(maxProperties), "maxProperties")
-                       .addIf(nn(minimum), "minimum")
+                       .addIf(nn(maximum), "maximum")
                        .addIf(nn(minItems), "minItems")
                        .addIf(nn(minLength), "minLength")
                        .addIf(nn(minProperties), "minProperties")
+                       .addIf(nn(minimum), "minimum")
                        .addIf(nn(multipleOf), "multipleOf")
                        .addIf(nn(not), "not")
                        .addIf(nn(nullable), "nullable")
diff --git 
a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/HeaderInfo.java
 
b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/HeaderInfo.java
index 9d9ea2a833..dfc7af071e 100644
--- 
a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/HeaderInfo.java
+++ 
b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/HeaderInfo.java
@@ -351,6 +351,7 @@ public class HeaderInfo extends SwaggerElement {
        public Set<String> keySet() {
                // @formatter:off
                var s = setb(String.class)
+                       .addIf(nn(ref), "$ref")
                        .addIf(nn(collectionFormat), "collectionFormat")
                        .addIf(nn(default_), "default")
                        .addIf(nn(description), "description")
@@ -360,15 +361,14 @@ public class HeaderInfo extends SwaggerElement {
                        .addIf(nn(exclusiveMinimum), "exclusiveMinimum")
                        .addIf(nn(format), "format")
                        .addIf(nn(items), "items")
-                       .addIf(nn(maximum), "maximum")
                        .addIf(nn(maxItems), "maxItems")
                        .addIf(nn(maxLength), "maxLength")
-                       .addIf(nn(minimum), "minimum")
+                       .addIf(nn(maximum), "maximum")
                        .addIf(nn(minItems), "minItems")
                        .addIf(nn(minLength), "minLength")
+                       .addIf(nn(minimum), "minimum")
                        .addIf(nn(multipleOf), "multipleOf")
                        .addIf(nn(pattern), "pattern")
-                       .addIf(nn(ref), "$ref")
                        .addIf(nn(type), "type")
                        .addIf(nn(uniqueItems), "uniqueItems")
                        .build();
diff --git 
a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/Items.java
 
b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/Items.java
index 0fba5a81e3..44e909c794 100644
--- 
a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/Items.java
+++ 
b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/Items.java
@@ -328,6 +328,7 @@ public class Items extends SwaggerElement {
        public Set<String> keySet() {
                // @formatter:off
                var s = setb(String.class)
+                       .addIf(nn(ref), "$ref")
                        .addIf(nn(collectionFormat), "collectionFormat")
                        .addIf(nn(default_), "default")
                        .addIf(isNotEmpty(enum_), "enum")
@@ -335,15 +336,14 @@ public class Items extends SwaggerElement {
                        .addIf(nn(exclusiveMinimum), "exclusiveMinimum")
                        .addIf(nn(format), "format")
                        .addIf(nn(items), "items")
-                       .addIf(nn(maximum), "maximum")
                        .addIf(nn(maxItems), "maxItems")
                        .addIf(nn(maxLength), "maxLength")
-                       .addIf(nn(minimum), "minimum")
+                       .addIf(nn(maximum), "maximum")
                        .addIf(nn(minItems), "minItems")
                        .addIf(nn(minLength), "minLength")
+                       .addIf(nn(minimum), "minimum")
                        .addIf(nn(multipleOf), "multipleOf")
                        .addIf(nn(pattern), "pattern")
-                       .addIf(nn(ref), "$ref")
                        .addIf(nn(type), "type")
                        .addIf(nn(uniqueItems), "uniqueItems")
                        .build();
diff --git 
a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/ParameterInfo.java
 
b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/ParameterInfo.java
index f622b6ef6e..f00dbaa152 100644
--- 
a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/ParameterInfo.java
+++ 
b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/ParameterInfo.java
@@ -550,12 +550,12 @@ public class ParameterInfo extends SwaggerElement {
                        .addIf(nn(format), "format")
                        .addIf(nn(in), "in")
                        .addIf(nn(items), "items")
-                       .addIf(nn(maximum), "maximum")
                        .addIf(nn(maxItems), "maxItems")
                        .addIf(nn(maxLength), "maxLength")
-                       .addIf(nn(minimum), "minimum")
+                       .addIf(nn(maximum), "maximum")
                        .addIf(nn(minItems), "minItems")
                        .addIf(nn(minLength), "minLength")
+                       .addIf(nn(minimum), "minimum")
                        .addIf(nn(multipleOf), "multipleOf")
                        .addIf(nn(name), "name")
                        .addIf(nn(pattern), "pattern")
diff --git 
a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/SchemaInfo.java
 
b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/SchemaInfo.java
index 3450de04ca..4c5d244271 100644
--- 
a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/SchemaInfo.java
+++ 
b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/SchemaInfo.java
@@ -557,6 +557,7 @@ public class SchemaInfo extends SwaggerElement {
        public Set<String> keySet() {
                // @formatter:off
                var s = setb(String.class)
+                       .addIf(nn(ref), "$ref")
                        .addIf(nn(additionalProperties), "additionalProperties")
                        .addIf(isNotEmpty(allOf), "allOf")
                        .addIf(nn(default_), "default")
@@ -569,19 +570,18 @@ public class SchemaInfo extends SwaggerElement {
                        .addIf(nn(externalDocs), "externalDocs")
                        .addIf(nn(format), "format")
                        .addIf(nn(items), "items")
-                       .addIf(nn(maximum), "maximum")
                        .addIf(nn(maxItems), "maxItems")
                        .addIf(nn(maxLength), "maxLength")
                        .addIf(nn(maxProperties), "maxProperties")
-                       .addIf(nn(minimum), "minimum")
+                       .addIf(nn(maximum), "maximum")
                        .addIf(nn(minItems), "minItems")
                        .addIf(nn(minLength), "minLength")
                        .addIf(nn(minProperties), "minProperties")
+                       .addIf(nn(minimum), "minimum")
                        .addIf(nn(multipleOf), "multipleOf")
                        .addIf(nn(pattern), "pattern")
                        .addIf(isNotEmpty(properties), "properties")
                        .addIf(nn(readOnly), "readOnly")
-                       .addIf(nn(ref), "$ref")
                        .addIf(nn(required), "required")
                        .addIf(isNotEmpty(requiredProperties), 
"requiredProperties")
                        .addIf(nn(title), "title")
diff --git a/juneau-core/juneau-bct/pom.xml b/juneau-core/juneau-bct/pom.xml
index 460f27b870..25f1489736 100644
--- a/juneau-core/juneau-bct/pom.xml
+++ b/juneau-core/juneau-bct/pom.xml
@@ -43,7 +43,8 @@
                <dependency>
                        <groupId>org.junit.jupiter</groupId>
                        <artifactId>junit-jupiter</artifactId>
-                       <version>6.0.1</version>
+                       <version>${junit.version}</version>
+                       <scope>provided</scope>
                </dependency>
                <dependency>
                        <groupId>org.opentest4j</groupId>
diff --git 
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
index 28707af99d..2189b3210f 100644
--- 
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
+++ 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
@@ -27,7 +27,6 @@ import java.util.*;
 import java.util.function.*;
 import java.util.stream.*;
 
-import org.apache.juneau.commons.function.*;
 import org.apache.juneau.commons.utils.*;
 import org.opentest4j.*;
 
@@ -118,53 +117,53 @@ import org.opentest4j.*;
  *       <js>"prop1,prop2"</js>, <js>"value1,value2"</js>);
  * </p>
  *
-        * <h5 class='section'>Customizing the Default Converter:</h5>
-        * <p>The default bean converter can be customized on a per-thread 
basis using:</p>
-        * <ul>
-        *    <li><b>{@link #setConverter(BeanConverter)}:</b> Set a custom 
converter for the current thread</li>
-        *    <li><b>{@link #resetConverter()}:</b> Reset to the system default 
converter</li>
-        * </ul>
-        *
-        * <p class='bjava'>
-        *    <jc>// Example: Set custom converter in @BeforeEach method</jc>
-        *    <ja>@BeforeEach</ja>
-        *    <jk>void</jk> <jsm>setUp</jsm>() {
-        *       <jk>var</jk> <jv>customConverter</jv> = 
BasicBeanConverter.<jsm>builder</jsm>()
-        *          .defaultSettings()
-        *          .addStringifier(LocalDate.<jk>class</jk>, <jp>date</jp> -> 
<jp>date</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>))
-        *          .addStringifier(MyType.<jk>class</jk>, <jp>obj</jp> -> 
<jp>obj</jp>.customFormat())
-        *          .build();
-        *       
BctAssertions.<jsm>setConverter</jsm>(<jv>customConverter</jv>);
-        *    }
-        *
-        *    <jc>// All assertions in this test class now use the custom 
converter</jc>
-        *    <ja>@Test</ja>
-        *    <jk>void</jk> <jsm>testWithCustomConverter</jsm>() {
-        *       <jsm>assertBean</jsm>(<jv>myObject</jv>, 
<js>"date,property"</js>, <js>"2023-12-01,value"</js>);
-        *    }
-        *
-        *    <jc>// Clean up in @AfterEach method</jc>
-        *    <ja>@AfterEach</ja>
-        *    <jk>void</jk> <jsm>tearDown</jsm>() {
-        *       BctAssertions.<jsm>resetConverter</jsm>();
-        *    }
-        * </p>
-        *
-        * <p class='bjava'>
-        *    <jc>// Example: Per-test method converter override</jc>
-        *    <ja>@Test</ja>
-        *    <jk>void</jk> <jsm>testSpecificFormat</jsm>() {
-        *       <jk>var</jk> <jv>dateConverter</jv> = 
BasicBeanConverter.<jsm>builder</jsm>()
-        *          .defaultSettings()
-        *          .addStringifier(LocalDateTime.<jk>class</jk>, <jp>dt</jp> 
-> <jp>dt</jp>.format(DateTimeFormatter.<jsf>ISO_DATE_TIME</jsf>))
-        *          .build();
-        *       BctAssertions.<jsm>setConverter</jsm>(<jv>dateConverter</jv>);
-        *       <jkt>try</jkt> {
-        *          <jsm>assertBean</jsm>(<jv>event</jv>, <js>"timestamp"</js>, 
<js>"2023-12-01T10:30:00"</js>);
-        *       } <jkt>finally</jkt> {
-        *          BctAssertions.<jsm>resetConverter</jsm>();
-        *       }
-        *    }
+ * <h5 class='section'>Customizing the Default Converter:</h5>
+ * <p>The default bean converter can be customized on a per-thread basis 
using:</p>
+ * <ul>
+ *    <li><b>{@link #setConverter(BeanConverter)}:</b> Set a custom converter 
for the current thread</li>
+ *    <li><b>{@link #resetConverter()}:</b> Reset to the system default 
converter</li>
+ * </ul>
+ *
+ * <p class='bjava'>
+ *    <jc>// Example: Set custom converter in @BeforeEach method</jc>
+ *    <ja>@BeforeEach</ja>
+ *    <jk>void</jk> <jsm>setUp</jsm>() {
+ *       <jk>var</jk> <jv>customConverter</jv> = 
BasicBeanConverter.<jsm>builder</jsm>()
+ *          .defaultSettings()
+ *          .addStringifier(LocalDate.<jk>class</jk>, <jp>date</jp> -> 
<jp>date</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>))
+ *          .addStringifier(MyType.<jk>class</jk>, <jp>obj</jp> -> 
<jp>obj</jp>.customFormat())
+ *          .build();
+ *       BctAssertions.<jsm>setConverter</jsm>(<jv>customConverter</jv>);
+ *    }
+ *
+ *    <jc>// All assertions in this test class now use the custom 
converter</jc>
+ *    <ja>@Test</ja>
+ *    <jk>void</jk> <jsm>testWithCustomConverter</jsm>() {
+ *       <jsm>assertBean</jsm>(<jv>myObject</jv>, <js>"date,property"</js>, 
<js>"2023-12-01,value"</js>);
+ *    }
+ *
+ *    <jc>// Clean up in @AfterEach method</jc>
+ *    <ja>@AfterEach</ja>
+ *    <jk>void</jk> <jsm>tearDown</jsm>() {
+ *       BctAssertions.<jsm>resetConverter</jsm>();
+ *    }
+ * </p>
+ *
+ * <p class='bjava'>
+ *    <jc>// Example: Per-test method converter override</jc>
+ *    <ja>@Test</ja>
+ *    <jk>void</jk> <jsm>testSpecificFormat</jsm>() {
+ *       <jk>var</jk> <jv>dateConverter</jv> = 
BasicBeanConverter.<jsm>builder</jsm>()
+ *          .defaultSettings()
+ *          .addStringifier(LocalDateTime.<jk>class</jk>, <jp>dt</jp> -> 
<jp>dt</jp>.format(DateTimeFormatter.<jsf>ISO_DATE_TIME</jsf>))
+ *          .build();
+ *       BctAssertions.<jsm>setConverter</jsm>(<jv>dateConverter</jv>);
+ *       <jkt>try</jkt> {
+ *          <jsm>assertBean</jsm>(<jv>event</jv>, <js>"timestamp"</js>, 
<js>"2023-12-01T10:30:00"</js>);
+ *       } <jkt>finally</jkt> {
+ *          BctAssertions.<jsm>resetConverter</jsm>();
+ *       }
+ *    }
  * </p>
  *
  * <h5 class='section'>Performance and Thread Safety:</h5>
@@ -183,12 +182,6 @@ import org.opentest4j.*;
  */
 public class BctAssertions {
 
-       // Thread-local memoized supplier for default converter (defaults to 
BasicBeanConverter.DEFAULT)
-       private static final ThreadLocal<ResettableSupplier<BeanConverter>> 
CONVERTER_SUPPLIER = ThreadLocal.withInitial(() -> memoizeResettable(() -> 
BasicBeanConverter.DEFAULT));
-
-       // Thread-local override for method-level converter customization
-       private static final ThreadLocal<BeanConverter> CONVERTER_OVERRIDE = 
new ThreadLocal<>();
-
        /**
         * Asserts that the fields/properties on the specified bean are the 
specified values after being converted to strings.
         *
@@ -391,7 +384,7 @@ public class BctAssertions {
                assertNotNull(actual, "Actual was null.");
                assertArgNotNull("fields", fields);
                assertArgNotNull("expected", expected);
-               var converter = getConverter();
+               var converter = BctConfiguration.getConverter();
                assertEquals(expected, tokenize(fields).stream().map(x -> 
converter.getNested(actual, x)).collect(joining(",")), composeMessage(message, 
"Bean assertion failed."));
        }
 
@@ -473,7 +466,7 @@ public class BctAssertions {
                assertArgNotNull("fields", fields);
                assertArgNotNull("expected", expected);
 
-               var converter = getConverter();
+               var converter = BctConfiguration.getConverter();
                var tokens = tokenize(fields);
                var errors = new ArrayList<AssertionFailedError>();
                List<Object> actualList = converter.listify(actual);
@@ -549,7 +542,7 @@ public class BctAssertions {
                assertArgNotNull("actual", actual);
                assertNotNull(actual, "Value was null.");
 
-               var a = getConverter().stringify(actual);
+               var a = BctConfiguration.getConverter().stringify(actual);
                assertTrue(a.contains(expected), composeMessage(message, 
"String did not contain expected substring.  ==> expected: <{0}> but was: 
<{1}>", expected, a));
        }
 
@@ -596,7 +589,7 @@ public class BctAssertions {
                assertArgNotNull("expected", expected);
                assertNotNull(actual, "Value was null.");
 
-               var a = getConverter().stringify(actual);
+               var a = BctConfiguration.getConverter().stringify(actual);
                var errors = new ArrayList<AssertionFailedError>();
 
                for (var e : expected) {
@@ -683,7 +676,7 @@ public class BctAssertions {
         */
        public static void assertEmpty(Supplier<String> message, Object value) {
                assertNotNull(value, "Value was null.");
-               var size = getConverter().size(value);
+               var size = BctConfiguration.getConverter().size(value);
                assertEquals(0, size, composeMessage(message, "Value was not 
empty. Size=<{0}>", size));
        }
 
@@ -761,7 +754,7 @@ public class BctAssertions {
                assertArgNotNull("expected", expected);
                assertArgNotNull("actual", actual);
 
-               var converter = getConverter();
+               var converter = BctConfiguration.getConverter();
                List<Object> list = converter.listify(actual);
                var errors = new ArrayList<AssertionFailedError>();
 
@@ -990,7 +983,7 @@ public class BctAssertions {
                assertArgNotNull("pattern", pattern);
                assertNotNull(value, "Value was null.");
 
-               var v = getConverter().stringify(value);
+               var v = BctConfiguration.getConverter().stringify(value);
                var m = StringUtils.getGlobMatchPattern(pattern).matcher(v);
                assertTrue(m.matches(), composeMessage(message, "Pattern 
didn''t match. ==> pattern: <{0}> but was: <{1}>", pattern, v));
        }
@@ -1056,7 +1049,7 @@ public class BctAssertions {
         */
        public static void assertNotEmpty(Supplier<String> message, Object 
value) {
                assertNotNull(value, "Value was null.");
-               int size = getConverter().size(value);
+               int size = BctConfiguration.getConverter().size(value);
                assertTrue(size > 0, composeMessage(message, "Value was 
empty."));
        }
 
@@ -1102,7 +1095,7 @@ public class BctAssertions {
         */
        public static void assertSize(Supplier<String> message, int expected, 
Object actual) {
                assertNotNull(actual, "Value was null.");
-               var size = getConverter().size(actual);
+               var size = BctConfiguration.getConverter().size(actual);
                assertEquals(expected, size, composeMessage(message, "Value not 
expected size."));
        }
 
@@ -1153,67 +1146,7 @@ public class BctAssertions {
                assertNotNull(actual, "Value was null.");
 
                var messageSupplier = message != null ? message : fs("");
-               assertEquals(expected, getConverter().stringify(actual), 
messageSupplier);
-       }
-
-       /**
-        * Resets the bean converter for the current thread to the system 
default.
-        *
-        * <p>This method clears any thread-local converter override set via 
{@link #setConverter(BeanConverter)},
-        * restoring the default converter ({@link BasicBeanConverter#DEFAULT}) 
for subsequent assertions.</p>
-        *
-        * <p>This is typically called in test teardown methods (e.g., {@code 
@AfterEach}) to clean up after tests
-        * that set a custom converter.</p>
-        *
-        * <h5 class='section'>Usage Example:</h5>
-        * <p class='bjava'>
-        *    <jc>// In @AfterEach method</jc>
-        *    BctAssertions.<jsm>resetConverter</jsm>();
-        * </p>
-        *
-        * <h5 class='section'>Thread Safety:</h5>
-        * <p>This method is thread-safe and only affects the current thread's 
converter.</p>
-        *
-        * @see #setConverter(BeanConverter)
-        */
-       public static void resetConverter() {
-               CONVERTER_OVERRIDE.remove();
-               CONVERTER_SUPPLIER.get().reset();
-       }
-
-       /**
-        * Sets a custom bean converter for the current thread.
-        *
-        * <p>This method allows you to override the default converter for all 
assertions in the current test method.
-        * The converter will be used by all assertion methods in the current 
thread.</p>
-        *
-        * <p>This is particularly useful in test setup methods (e.g., {@code 
@BeforeEach}) to configure a custom converter
-        * for all tests in a test class or method.</p>
-        *
-        * <h5 class='section'>Usage Example:</h5>
-        * <p class='bjava'>
-        *    <jc>// In @BeforeEach method</jc>
-        *    <jk>var</jk> <jv>customConverter</jv> = 
BasicBeanConverter.<jsm>builder</jsm>()
-        *       .defaultSettings()
-        *       .addStringifier(LocalDate.<jk>class</jk>, <jp>date</jp> -> 
<jp>date</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>))
-        *       .build();
-        *    BctAssertions.<jsm>setConverter</jsm>(<jv>customConverter</jv>);
-        *
-        *    <jc>// All subsequent assertions in this test method will use the 
custom converter</jc>
-        *    <jsm>assertBean</jsm>(<jv>event</jv>, <js>"date"</js>, 
<js>"2023-12-01"</js>);
-        * </p>
-        *
-        * <h5 class='section'>Thread Safety:</h5>
-        * <p>This method is thread-safe and uses thread-local storage. Each 
test method running in parallel
-        * will have its own converter instance, preventing cross-thread 
interference.</p>
-        *
-        * @param converter The bean converter to use for the current thread. 
Must not be <jk>null</jk>.
-        * @throws IllegalArgumentException If converter is <jk>null</jk>.
-        * @see #resetConverter()
-        */
-       public static void setConverter(BeanConverter converter) {
-               assertArgNotNull("converter", converter);
-               CONVERTER_OVERRIDE.set(converter);
+               assertEquals(expected, 
BctConfiguration.getConverter().stringify(actual), messageSupplier);
        }
 
        /**
@@ -1234,21 +1167,5 @@ public class BctAssertions {
                return fs("{0}, Caused by: {1}", customMessage.get(), 
f(defaultMessage, defaultArgs));
        }
 
-       /**
-        * Gets the bean converter for the current thread.
-        *
-        * <p>Returns the thread-local converter override if set, otherwise 
returns the memoized default converter.
-        * This method is used internally by all assertion methods to get the 
current thread-local converter.</p>
-        *
-        * @return The bean converter to use for the current thread.
-        */
-       private static BeanConverter getConverter() {
-               var override = CONVERTER_OVERRIDE.get();
-               if (override != null) {
-                       return override;
-               }
-               return CONVERTER_SUPPLIER.get().get();
-       }
-
        private BctAssertions() {}
 }
\ No newline at end of file
diff --git 
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctConfiguration.java
 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctConfiguration.java
new file mode 100644
index 0000000000..47dc749466
--- /dev/null
+++ 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctConfiguration.java
@@ -0,0 +1,309 @@
+/*
+ * 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.bct;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import org.apache.juneau.commons.function.*;
+import org.apache.juneau.commons.settings.*;
+
+/**
+ * Configuration utility for Bean-Centric Testing (BCT) framework.
+ *
+ * <p>
+ * This class provides thread-local configuration settings for BCT assertions, 
allowing test-specific
+ * customization of behavior such as map sorting, collection sorting, and bean 
converter selection.
+ *
+ * <p>
+ * Configuration is managed using thread-local storage, ensuring that parallel 
test execution doesn't
+ * interfere with each other's settings. Settings are typically configured via 
the {@link org.apache.juneau.junit.bct.annotations.BctConfig @BctConfig}
+ * annotation, but can also be set programmatically.
+ *
+ * <h5 class='section'>Configuration Properties:</h5>
+ * <ul>
+ *     <li><b>{@link #BCT_SORT_MAPS}:</b> Enable sorting of maps in 
assertions</li>
+ *     <li><b>{@link #BCT_SORT_COLLECTIONS}:</b> Enable sorting of collections 
in assertions</li>
+ *     <li><b>Bean Converter:</b> Custom bean converter instance for property 
access and conversion</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage via Annotation:</h5>
+ * <p class='bjava'>
+ *     <ja>@BctConfig</ja>(sortMaps=TriState.<jsf>TRUE</jsf>, 
beanConverter=MyConverter.<jk>class</jk>)
+ *     <jk>class</jk> MyTest {
+ *             <ja>@Test</ja>
+ *             <ja>@BctConfig</ja>(sortCollections=TriState.<jsf>TRUE</jsf>)
+ *             <jk>void</jk> testSomething() {
+ *                     <jc>// sortMaps=true (from class), sortCollections=true 
(from method),</jc>
+ *                     <jc>// beanConverter=MyConverter (from class)</jc>
+ *             }
+ *     }
+ * </p>
+ *
+ * <h5 class='section'>Usage via Programmatic API:</h5>
+ * <p class='bjava'>
+ *     <ja>@BeforeEach</ja>
+ *     <jk>void</jk> setUp() {
+ *             
BctConfiguration.<jsm>set</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, 
<jk>true</jk>);
+ *             BctConfiguration.<jsm>set</jsm>(<jk>new</jk> 
MyCustomConverter());
+ *     }
+ *
+ *     <ja>@AfterEach</ja>
+ *     <jk>void</jk> tearDown() {
+ *             BctConfiguration.<jsm>clear</jsm>();
+ *     }
+ * </p>
+ *
+ * <h5 class='section'>Thread Safety:</h5>
+ * <p>
+ * All methods in this class are thread-safe. Configuration is stored in 
thread-local storage,
+ * ensuring that each test thread has its own isolated configuration that 
doesn't interfere
+ * with other concurrently running tests.
+ * </p>
+ *
+ * @see org.apache.juneau.junit.bct.annotations.BctConfig
+ * @see org.apache.juneau.junit.bct.BctAssertions
+ */
+public class BctConfiguration {
+
+       // Thread-local memoized supplier for default converter (defaults to 
BasicBeanConverter.DEFAULT)
+       private static final ThreadLocal<ResettableSupplier<BeanConverter>> 
CONVERTER_SUPPLIER = ThreadLocal.withInitial(() -> memoizeResettable(() -> 
BasicBeanConverter.DEFAULT));
+
+       /**
+        * Configuration property name for enabling map sorting in BCT 
assertions.
+        *
+        * <p>
+        * When set to <jk>true</jk>, maps will be sorted by key before 
comparison in assertions.
+        * This ensures consistent ordering regardless of the map's internal 
ordering.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      
BctConfiguration.<jsm>set</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, 
<jk>true</jk>);
+        * </p>
+        */
+       public static final String BCT_SORT_MAPS = "Bct.sortMaps";
+
+       /**
+        * Configuration property name for enabling collection sorting in BCT 
assertions.
+        *
+        * <p>
+        * When set to <jk>true</jk>, collections will be sorted before 
comparison in assertions.
+        * This ensures consistent ordering regardless of the collection's 
internal ordering.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      
BctConfiguration.<jsm>set</jsm>(BctConfiguration.<jsf>BCT_SORT_COLLECTIONS</jsf>,
 <jk>true</jk>);
+        * </p>
+        */
+       public static final String BCT_SORT_COLLECTIONS = "Bct.sortCollections";
+
+       // Thread-local override for method-level converter customization
+       private static final ThreadLocal<BeanConverter> CONVERTER_OVERRIDE = 
new ThreadLocal<>();
+
+       private static final Settings SETTINGS = Settings.create().build();
+
+       /**
+        * Sets a thread-local configuration value.
+        *
+        * <p>
+        * The value is stored in thread-local storage and will only affect the 
current thread.
+        * This is the recommended way to set configuration for individual 
tests.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      
BctConfiguration.<jsm>set</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, 
<jk>true</jk>);
+        * </p>
+        *
+        * @param name The configuration property name (e.g., {@link 
#BCT_SORT_MAPS}).
+        * @param value The value to set.
+        * @see #get(String)
+        * @see #clear()
+        */
+       public static void set(String name, Object value) {
+               SETTINGS.setLocal(name, s(value));
+       }
+
+       /**
+        * Sets a global configuration value.
+        *
+        * <p>
+        * Global values apply to all threads and persist until explicitly 
cleared.
+        * Use with caution in multi-threaded test environments.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      
BctConfiguration.<jsm>setGlobal</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>,
 <jk>true</jk>);
+        * </p>
+        *
+        * @param name The configuration property name (e.g., {@link 
#BCT_SORT_MAPS}).
+        * @param value The value to set.
+        * @see #get(String)
+        * @see #clearGlobal()
+        */
+       public static void setGlobal(String name, Object value) {
+               SETTINGS.setGlobal(name, s(value));
+       }
+
+       /**
+        * Gets a configuration setting by name.
+        *
+        * <p>
+        * Returns the setting object which can be used to check if a value is 
set and retrieve it.
+        * Thread-local values take precedence over global values.
+        *
+        * @param name The configuration property name (e.g., {@link 
#BCT_SORT_MAPS}).
+        * @return The setting object, or <jk>null</jk> if not set.
+        * @see #get(String, Object)
+        * @see #set(String, Object)
+        */
+       public static StringSetting get(String name) {
+               return SETTINGS.get(name);
+       }
+
+       /**
+        * Gets a configuration value by name with a default value.
+        *
+        * <p>
+        * Returns the configured value if set, otherwise returns the provided 
default value.
+        * Thread-local values take precedence over global values.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      <jk>boolean</jk> <jv>sortMaps</jv> = 
BctConfiguration.<jsm>get</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, 
<jk>false</jk>);
+        * </p>
+        *
+        * @param <T> The type of the value.
+        * @param name The configuration property name (e.g., {@link 
#BCT_SORT_MAPS}).
+        * @param def The default value to return if not set.
+        * @return The configured value, or the default if not set.
+        * @see #get(String)
+        * @see #set(String, Object)
+        */
+       public static <T> T get(String name, T def) {
+               return SETTINGS.get(name, def);
+       }
+
+       /**
+        * Clears all thread-local configuration settings.
+        *
+        * <p>
+        * This method removes all thread-local settings including:
+        * <ul>
+        *      <li>Configuration properties (e.g., {@link #BCT_SORT_MAPS}, 
{@link #BCT_SORT_COLLECTIONS})</li>
+        *      <li>Bean converter override</li>
+        * </ul>
+        *
+        * <p>
+        * This is typically called in test teardown methods (e.g., {@code 
@AfterEach}) to ensure
+        * a clean state for subsequent tests. The {@link 
org.apache.juneau.junit.bct.annotations.BctConfigExtension BctConfigExtension}
+        * automatically calls this method after each test.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      <ja>@AfterEach</ja>
+        *      <jk>void</jk> tearDown() {
+        *              BctConfiguration.<jsm>clear</jsm>();
+        *      }
+        * </p>
+        *
+        * @see #clearGlobal()
+        * @see #set(String, Object)
+        */
+       public static void clear() {
+               SETTINGS.clearLocal();
+               CONVERTER_OVERRIDE.remove();
+       }
+
+       /**
+        * Clears all global configuration settings.
+        *
+        * <p>
+        * This method removes all global settings. Use with caution as this 
affects all threads.
+        *
+        * @see #clear()
+        * @see #setGlobal(String, Object)
+        */
+       public static void clearGlobal() {
+               SETTINGS.clearGlobal();
+       }
+
+       /**
+        * Sets a custom bean converter for the current thread.
+        *
+        * <p>
+        * This method allows you to override the default converter ({@link 
BasicBeanConverter#DEFAULT}) for all
+        * assertions in the current test method. The converter will be used by 
all assertion methods in the current thread.
+        *
+        * <p>
+        * This is particularly useful when you need custom property access, 
stringification, or conversion logic
+        * for specific tests. The converter can be configured via the {@link 
org.apache.juneau.junit.bct.annotations.BctConfig @BctConfig}
+        * annotation or set programmatically.
+        *
+        * <h5 class='section'>Usage Example:</h5>
+        * <p class='bjava'>
+        *      <ja>@BeforeEach</ja>
+        *      <jk>void</jk> setUp() {
+        *              <jk>var</jk> <jv>customConverter</jv> = 
BasicBeanConverter.<jsm>builder</jsm>()
+        *                      .defaultSettings()
+        *                      .addStringifier(LocalDate.<jk>class</jk>, 
<jp>date</jp> -> 
<jp>date</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>))
+        *                      .build();
+        *              
BctConfiguration.<jsm>set</jsm>(<jv>customConverter</jv>);
+        *      }
+        *
+        *      <ja>@Test</ja>
+        *      <jk>void</jk> testWithCustomConverter() {
+        *              <jc>// All assertions use the custom converter</jc>
+        *              <jsm>assertBean</jsm>(<jv>event</jv>, <js>"date"</js>, 
<js>"2023-12-01"</js>);
+        *      }
+        *
+        *      <ja>@AfterEach</ja>
+        *      <jk>void</jk> tearDown() {
+        *              BctConfiguration.<jsm>clear</jsm>(); <jc>// Also clears 
converter override</jc>
+        *      }
+        * </p>
+        *
+        * <h5 class='section'>Thread Safety:</h5>
+        * <p>
+        * This method is thread-safe and uses thread-local storage. Each test 
method running in parallel
+        * will have its own converter instance, preventing cross-thread 
interference.
+        * </p>
+        *
+        * @param converter The bean converter to use for the current thread. 
Must not be <jk>null</jk>.
+        * @throws IllegalArgumentException If converter is <jk>null</jk>.
+        * @see #clear()
+        * @see 
org.apache.juneau.junit.bct.annotations.BctConfig#beanConverter()
+        * @see BeanConverter
+        * @see BasicBeanConverter
+        */
+       public static void set(BeanConverter converter) {
+               assertArgNotNull("converter", converter);
+               CONVERTER_OVERRIDE.set(converter);
+       }
+
+
+       /**
+        * Gets the bean converter for the current thread.
+        *
+        * <p>Returns the thread-local converter override if set, otherwise 
returns the memoized default converter.
+        * This method is used internally by all assertion methods to get the 
current thread-local converter.</p>
+        *
+        * @return The bean converter to use for the current thread.
+        */
+       static BeanConverter getConverter() {
+               return 
opt(BctConfiguration.CONVERTER_OVERRIDE.get()).orElseGet(CONVERTER_SUPPLIER.get());
+       }
+}
diff --git 
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/Listifiers.java
 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/Listifiers.java
index 35bd0045dd..917917dfd9 100644
--- 
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/Listifiers.java
+++ 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/Listifiers.java
@@ -20,6 +20,7 @@ import static java.util.Collections.*;
 import static java.util.Spliterators.*;
 import static java.util.stream.StreamSupport.*;
 import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.junit.bct.BctConfiguration.*;
 
 import java.util.*;
 import java.util.stream.*;
@@ -134,7 +135,7 @@ public class Listifiers {
        @SuppressWarnings("unchecked")
        public static Listifier<Collection> collectionListifier() {
                return (bc, collection) -> {
-                       if (collection instanceof Set && ! (collection 
instanceof SortedSet) && ! (collection instanceof LinkedHashSet)) {
+                       if (collection instanceof Set && ! (collection 
instanceof SortedSet) && ! (collection instanceof LinkedHashSet) && 
BctConfiguration.get(BCT_SORT_COLLECTIONS, false)) {
                                // TODO - This is too unreliable.
                                var collection2 = new 
TreeSet<>(flexibleComparator(bc));
                                collection2.addAll(collection);
@@ -314,8 +315,7 @@ public class Listifiers {
        @SuppressWarnings("unchecked")
        public static Listifier<Map> mapListifier() {
                return (bc, map) -> {
-                       if (! (map instanceof SortedMap) && ! (map instanceof 
LinkedHashMap)) {
-                               // TODO - This is too unreliable.
+                       if (! (map instanceof SortedMap) && ! (map instanceof 
LinkedHashMap) && BctConfiguration.get(BCT_SORT_MAPS, false)) {
                                var map2 = new 
TreeMap<>(flexibleComparator(bc));
                                map2.putAll(map);
                                map = map2;
diff --git 
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/annotations/BctConfig.java
 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/annotations/BctConfig.java
new file mode 100644
index 0000000000..e3224c4f4b
--- /dev/null
+++ 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/annotations/BctConfig.java
@@ -0,0 +1,90 @@
+/*
+ * 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.bct.annotations;
+
+import java.lang.annotation.*;
+
+import org.apache.juneau.commons.lang.*;
+import org.apache.juneau.junit.bct.*;
+import org.junit.jupiter.api.extension.*;
+
+/**
+ * Annotation for configuring BCT settings in JUnit 5 tests.
+ *
+ * <p>
+ * This annotation can be applied to test classes or test methods to 
automatically configure
+ * BCT settings before tests run and clear them after tests complete.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ *     <ja>@BctConfig</ja>(sortMaps=TriState.<jsf>TRUE</jsf>)
+ *     <jk>class</jk> MyTest {
+ *             <ja>@Test</ja>
+ *             <ja>@BctConfig</ja>(sortCollections=TriState.<jsf>TRUE</jsf>)
+ *             <jk>void</jk> testFoo() {
+ *                     <jc>// sortMaps and sortCollections are both 
enabled</jc>
+ *             }
+ *     }
+ * </p>
+ *
+ * <p>
+ * Method-level annotations override class-level annotations.
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(BctConfigExtension.class)
+public @interface BctConfig {
+
+       /**
+        * Enable sorting of maps in BCT assertions.
+        *
+        * @return {@link TriState#TRUE} to enable map sorting, {@link 
TriState#FALSE} to disable,
+        *      or {@link TriState#UNSET} to inherit from class-level 
annotation.
+        */
+       TriState sortMaps() default TriState.UNSET;
+
+       /**
+        * Enable sorting of collections in BCT assertions.
+        *
+        * @return {@link TriState#TRUE} to enable collection sorting, {@link 
TriState#FALSE} to disable,
+        *      or {@link TriState#UNSET} to inherit from class-level 
annotation.
+        */
+       TriState sortCollections() default TriState.UNSET;
+
+       /**
+        * Custom bean converter class to use for BCT assertions.
+        *
+        * <p>
+        * If specified (not the default {@link BeanConverter}), the class will 
be instantiated using
+        * its no-arg constructor and set as the converter for the current 
thread.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      
<ja>@BctConfiguration</ja>(beanConverter=MyCustomConverter.<jk>class</jk>)
+        *      <jk>class</jk> MyTest {
+        *              <ja>@Test</ja>
+        *              <jk>void</jk> testFoo() {
+        *                      <jc>// Uses MyCustomConverter for all 
assertions</jc>
+        *              }
+        *      }
+        * </p>
+        *
+        * @return The bean converter class to instantiate and use, or {@link 
BeanConverter} to use the default.
+        */
+       Class<? extends BeanConverter> beanConverter() default 
BeanConverter.class;
+}
+
diff --git 
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/annotations/BctConfigExtension.java
 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/annotations/BctConfigExtension.java
new file mode 100644
index 0000000000..c606f3081b
--- /dev/null
+++ 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/annotations/BctConfigExtension.java
@@ -0,0 +1,76 @@
+/*
+ * 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.bct.annotations;
+
+import static org.apache.juneau.commons.utils.ThrowableUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+import static org.apache.juneau.junit.bct.BctConfiguration.*;
+import static org.apache.juneau.commons.lang.TriState.*;
+
+import java.util.*;
+
+import org.apache.juneau.junit.bct.*;
+import org.junit.jupiter.api.extension.*;
+
+/**
+ * JUnit 5 extension that processes {@link BctConfig} annotations.
+ *
+ * <p>
+ * This extension automatically sets BCT settings before test execution and 
clears them after.
+ * It supports both class-level and method-level annotations, with 
method-level taking precedence.
+ */
+public class BctConfigExtension implements BeforeEachCallback, 
AfterEachCallback {
+
+       @Override
+       public void beforeEach(ExtensionContext context) throws Exception {
+               var al = getAnnotations(context);
+
+               if (al.isEmpty())
+                       return;
+
+               clear();
+
+               al.stream().map(x -> x.sortMaps()).filter(x -> ne(x, 
UNSET)).findFirst().ifPresent(x -> set(BCT_SORT_MAPS, x == TRUE));
+               al.stream().map(x -> x.sortCollections()).filter(x -> ne(x, 
UNSET)).findFirst().ifPresent(x -> set(BCT_SORT_COLLECTIONS, x == TRUE));
+               al.stream().map(x -> x.beanConverter()).filter(x -> ne(x, 
BeanConverter.class)).findFirst().map(x -> eq(x, BasicBeanConverter.class) ? 
null : x).ifPresent(x -> setConverter(x));
+       }
+
+       private static void setConverter(Class<? extends BeanConverter> x) {
+               safe(()->{
+                       var c = x.getDeclaredConstructor();
+                       c.setAccessible(true);
+                       set(c.newInstance());
+               }, e -> rex(e, "Failed to instantiate BeanConverter: {0}. It 
must have a no-arg constructor.", x.getName()));
+       }
+
+       @Override
+       public void afterEach(ExtensionContext context) throws Exception {
+               var al = getAnnotations(context);
+
+               if (al.isEmpty())
+                       return;
+
+               clear();
+       }
+
+       private static List<BctConfig> getAnnotations(ExtensionContext context) 
{
+               var l = new ArrayList<BctConfig>();
+               context.getTestMethod().map(x -> 
x.getAnnotation(BctConfig.class)).ifPresent(x -> l.add(x));
+               context.getTestClass().map(x -> 
x.getAnnotation(BctConfig.class)).ifPresent(x -> l.add(x));
+               return l;
+       }
+}
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/lang/TriState.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/lang/TriState.java
new file mode 100644
index 0000000000..6b668f96fb
--- /dev/null
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/lang/TriState.java
@@ -0,0 +1,77 @@
+/*
+ * 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.commons.lang;
+
+/**
+ * Three-state enumeration for boolean-like values that can be explicitly set 
or unset.
+ *
+ * <p>
+ * This enum is useful in scenarios where you need to distinguish between:
+ * <ul>
+ *     <li>A value that is explicitly set to <jk>true</jk></li>
+ *     <li>A value that is explicitly set to <jk>false</jk></li>
+ *     <li>A value that is not set (should inherit from a parent/default)</li>
+ * </ul>
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ *     <jc>// In an annotation</jc>
+ *     <jk>public @interface</jk> MyConfig {
+ *             TriState <jsm>enabled</jsm>() <jk>default</jk> 
TriState.<jsf>UNSET</jsf>;
+ *     }
+ *
+ *     <jc>// Usage</jc>
+ *     <ja>@MyConfig</ja>(enabled = TriState.<jsf>TRUE</jsf>)  <jc>// 
Explicitly enabled</jc>
+ *     <ja>@MyConfig</ja>(enabled = TriState.<jsf>FALSE</jsf>) <jc>// 
Explicitly disabled</jc>
+ *     <ja>@MyConfig</ja>                                    <jc>// Not set, 
inherits default</jc>
+ * </p>
+ */
+public enum TriState {
+       /** Explicitly set to <jk>true</jk>. */
+       TRUE,
+
+       /** Explicitly set to <jk>false</jk>. */
+       FALSE,
+
+       /** Not set - should inherit from parent or use default value. */
+       UNSET;
+
+       /**
+        * Converts this TriState to a boolean value.
+        *
+        * <p>
+        * If this is {@link #UNSET}, returns the provided default value.
+        *
+        * @param defaultValue The default value to use if this is {@link 
#UNSET}.
+        * @return <jk>true</jk> if this is {@link #TRUE}, <jk>false</jk> if 
this is {@link #FALSE},
+        *      or the provided default if this is {@link #UNSET}.
+        */
+       public boolean toBoolean(boolean defaultValue) {
+               return this == UNSET ? defaultValue : (this == TRUE);
+       }
+
+       /**
+        * Converts a boolean to a TriState.
+        *
+        * @param value The boolean value.
+        * @return {@link #TRUE} if <jk>true</jk>, {@link #FALSE} if 
<jk>false</jk>.
+        */
+       public static TriState fromBoolean(boolean value) {
+               return value ? TRUE : FALSE;
+       }
+}
+
diff --git a/juneau-docs/docs/topics/07.01.00.JuneauBctBasics.md 
b/juneau-docs/docs/topics/07.01.00.JuneauBctBasics.md
index e8187f5ae2..9a593e8408 100644
--- a/juneau-docs/docs/topics/07.01.00.JuneauBctBasics.md
+++ b/juneau-docs/docs/topics/07.01.00.JuneauBctBasics.md
@@ -18,11 +18,12 @@ A powerful and intuitive testing framework that extends 
JUnit with streamlined a
 
 ## 📚 Advanced Topics
 
-- [Stringifiers](07.01.01.Stringifiers.md) - Custom string conversions
-- [Listifiers](07.01.02.Listifiers.md) - Custom list conversions
-- [Swappers](07.01.03.Swappers.md) - Object transformation
+- [Custom Error Messages](07.02.00.CustomErrorMessages.md) - Enhanced error 
reporting
+- [Customization](07.03.00.Customization.md) - Configuration via @BctConfig 
and BctConfiguration
+  - [Stringifiers](07.03.01.Stringifiers.md) - Custom string conversions
+  - [Listifiers](07.03.02.Listifiers.md) - Custom list conversions
+  - [Swappers](07.03.03.Swappers.md) - Object transformation
 - [Property Extractors](07.01.04.PropertyExtractors.md) - Custom property 
access
-- [Custom Error Messages](07.01.05.CustomErrorMessages.md) - Enhanced error 
reporting
 
 ## Overview
 
@@ -536,9 +537,33 @@ assertBean(user, "orders{size,#{total}}", 
"{3,[{99.99},{149.99},{79.99}]}");
 
 BCT provides several advanced configuration options for customizing assertion 
behavior:
 
+### @BctConfig Annotation
+
+The `@BctConfig` annotation provides a declarative way to configure BCT 
settings for your tests. It can be applied at the class level or method level, 
with method-level annotations taking precedence.
+
+```java
+@BctConfig(sortMaps=TriState.TRUE, beanConverter=MyCustomConverter.class)
+class MyTest {
+    @Test
+    @BctConfig(sortCollections=TriState.TRUE)
+    void testSomething() {
+        // sortMaps=true (from class), sortCollections=true (from method),
+        // beanConverter=MyCustomConverter (from class)
+        assertBean(order, "items{0{name}}", "{{Laptop}}");
+    }
+}
+```
+
+The annotation supports:
+- **sortMaps**: Enable sorting of map entries before comparison
+- **sortCollections**: Enable sorting of collection elements before comparison
+- **beanConverter**: Specify a custom bean converter class to use
+
+For more details, see [Customization](07.03.00.Customization.md).
+
 ### Custom Bean Converters
 
-The default bean converter can be customized on a per-thread basis using 
`setConverter()` and `resetConverter()`. This is particularly useful in test 
setup methods to configure a custom converter for all tests in a test class or 
method.
+The default bean converter can be customized on a per-thread basis using 
`BctConfiguration.set(BeanConverter)` or via the `@BctConfig` annotation. This 
is particularly useful in test setup methods to configure a custom converter 
for all tests in a test class or method.
 
 ```java
 // Set custom converter in @BeforeEach method
@@ -551,7 +576,7 @@ var converter = BasicBeanConverter.builder()
     .addStringifier(Money.class, money -> 
         money.getAmount().toPlainString())
     .build();
-    BctAssertions.setConverter(converter);
+    BctConfiguration.set(converter);
 }
 
 // All assertions now use the custom converter
@@ -563,7 +588,7 @@ void testWithCustomConverter() {
 // Reset in @AfterEach method
 @AfterEach
 void tearDown() {
-    BctAssertions.resetConverter();
+    BctConfiguration.clear();
 }
 ```
 
@@ -573,11 +598,12 @@ void tearDown() {
 
 For detailed information on extending and customizing BCT, see:
 
-- **[Stringifiers](07.01.01.Stringifiers.md)** - Define how objects are 
converted to strings for comparison
-- **[Listifiers](07.01.02.Listifiers.md)** - Convert collection-like objects 
to lists for testing
-- **[Swappers](07.01.03.Swappers.md)** - Transform objects before processing 
(unwrap Optional, Supplier, etc.)
+- **[Custom Error Messages](07.02.00.CustomErrorMessages.md)** - Add 
contextual information to assertion failures
+- **[Customization](07.03.00.Customization.md)** - Configure BCT via 
@BctConfig annotation and BctConfiguration class
+  - **[Stringifiers](07.03.01.Stringifiers.md)** - Define how objects are 
converted to strings for comparison
+  - **[Listifiers](07.03.02.Listifiers.md)** - Convert collection-like objects 
to lists for testing
+  - **[Swappers](07.03.03.Swappers.md)** - Transform objects before processing 
(unwrap Optional, Supplier, etc.)
 - **[Property Extractors](07.01.04.PropertyExtractors.md)** - Define custom 
property access logic for non-standard objects
-- **[Custom Error Messages](07.01.05.CustomErrorMessages.md)** - Add 
contextual information to assertion failures
 
 ## Migration Examples
 
diff --git a/juneau-docs/docs/topics/07.01.04.PropertyExtractors.md 
b/juneau-docs/docs/topics/07.01.04.PropertyExtractors.md
index 4f24da3162..1a9752c23f 100644
--- a/juneau-docs/docs/topics/07.01.04.PropertyExtractors.md
+++ b/juneau-docs/docs/topics/07.01.04.PropertyExtractors.md
@@ -480,8 +480,9 @@ try {
 
 ## See Also
 
-- [Stringifiers](07.01.01.Stringifiers.md) - Converting objects to strings
-- [Listifiers](07.01.02.Listifiers.md) - Converting collection-like objects to 
lists
-- [Swappers](07.01.03.Swappers.md) - Transforming objects before processing
 - [juneau-bct Basics](07.01.00.JuneauBctBasics.md) - Main BCT documentation
+- [Customization](07.03.00.Customization.md) - Configuration via @BctConfig 
and BctConfiguration
+  - [Stringifiers](07.03.01.Stringifiers.md) - Converting objects to strings
+  - [Listifiers](07.03.02.Listifiers.md) - Converting collection-like objects 
to lists
+  - [Swappers](07.03.03.Swappers.md) - Transforming objects before processing
 
diff --git a/juneau-docs/docs/topics/07.01.05.CustomErrorMessages.md 
b/juneau-docs/docs/topics/07.02.00.CustomErrorMessages.md
similarity index 96%
rename from juneau-docs/docs/topics/07.01.05.CustomErrorMessages.md
rename to juneau-docs/docs/topics/07.02.00.CustomErrorMessages.md
index 119ddd2e4a..0a73632cbd 100644
--- a/juneau-docs/docs/topics/07.01.05.CustomErrorMessages.md
+++ b/juneau-docs/docs/topics/07.02.00.CustomErrorMessages.md
@@ -398,9 +398,10 @@ void testOrderIntegration() {
 
 ## See Also
 
-- [Stringifiers](07.01.01.Stringifiers.md) - Converting objects to strings
-- [Listifiers](07.01.02.Listifiers.md) - Converting collection-like objects to 
lists
-- [Swappers](07.01.03.Swappers.md) - Transforming objects before processing
-- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property 
access logic
 - [juneau-bct Basics](07.01.00.JuneauBctBasics.md) - Main BCT documentation
+- [Customization](07.03.00.Customization.md) - Configuration via @BctConfig 
and BctConfiguration
+  - [Stringifiers](07.03.01.Stringifiers.md) - Converting objects to strings
+  - [Listifiers](07.03.02.Listifiers.md) - Converting collection-like objects 
to lists
+  - [Swappers](07.03.03.Swappers.md) - Transforming objects before processing
+- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property 
access logic
 
diff --git a/juneau-docs/docs/topics/07.03.00.Customization.md 
b/juneau-docs/docs/topics/07.03.00.Customization.md
new file mode 100644
index 0000000000..43b1db7ac8
--- /dev/null
+++ b/juneau-docs/docs/topics/07.03.00.Customization.md
@@ -0,0 +1,351 @@
+---
+title: "Customization"
+slug: Customization
+---
+
+# Customization
+
+BCT provides flexible customization options through the `@BctConfig` 
annotation and the `BctConfiguration` class. These tools allow you to configure 
assertion behavior, customize property access, and extend BCT's capabilities to 
match your testing needs.
+
+## 📋 Table of Contents
+
+- [@BctConfig Annotation](#bctconfig-annotation)
+- [BctConfiguration Class](#bctconfiguration-class)
+- [Configuration Properties](#configuration-properties)
+- [Custom Bean Converters](#custom-bean-converters)
+- [Thread Safety](#thread-safety)
+- [Advanced Customization Topics](#advanced-customization-topics)
+
+## @BctConfig Annotation
+
+The `@BctConfig` annotation provides a declarative way to configure BCT 
settings for your tests. It can be applied at both the class level and method 
level, with method-level annotations taking precedence over class-level 
annotations.
+
+### Basic Usage
+
+```java
+import org.apache.juneau.junit.bct.annotations.BctConfig;
+import org.apache.juneau.commons.lang.TriState;
+
+@BctConfig(sortMaps=TriState.TRUE)
+class MyTest {
+    @Test
+    @BctConfig(sortCollections=TriState.TRUE)
+    void testSomething() {
+        // sortMaps=true (from class), sortCollections=true (from method)
+        assertBean(order, "items{0{name}}", "{{Laptop}}");
+    }
+}
+```
+
+### Annotation Properties
+
+#### sortMaps
+
+Controls whether map entries are sorted by key before comparison in assertions.
+
+```java
+@BctConfig(sortMaps=TriState.TRUE)
+class MyTest {
+    @Test
+    void testMap() {
+        // Map entries will be sorted before comparison
+        Map<String, String> map = Map.of("zebra", "animal", "apple", "fruit");
+        assertBean(map, "apple,zebra", "fruit,animal");
+    }
+}
+```
+
+**Values:**
+- `TriState.TRUE` - Enable map sorting
+- `TriState.FALSE` - Disable map sorting
+- `TriState.UNSET` - Inherit from class-level annotation (default for 
method-level)
+
+#### sortCollections
+
+Controls whether collection elements are sorted before comparison in 
assertions.
+
+```java
+@BctConfig(sortCollections=TriState.TRUE)
+class MyTest {
+    @Test
+    void testList() {
+        // List elements will be sorted before comparison
+        List<String> list = Arrays.asList("zebra", "apple", "banana");
+        assertList(list, "apple", "banana", "zebra");
+    }
+}
+```
+
+**Values:**
+- `TriState.TRUE` - Enable collection sorting
+- `TriState.FALSE` - Disable collection sorting
+- `TriState.UNSET` - Inherit from class-level annotation (default for 
method-level)
+
+#### beanConverter
+
+Specifies a custom bean converter class to use for all assertions in the test.
+
+```java
+public static class MyCustomConverter extends BasicBeanConverter {
+    public MyCustomConverter() {
+        super(BasicBeanConverter.builder()
+            .defaultSettings()
+            .addStringifier(LocalDate.class, date -> 
+                date.format(DateTimeFormatter.ISO_LOCAL_DATE))
+            .build());
+    }
+}
+
+@BctConfig(beanConverter=MyCustomConverter.class)
+class MyTest {
+    @Test
+    void testWithCustomConverter() {
+        // All assertions use MyCustomConverter
+        assertBean(event, "date", "2023-12-01");
+    }
+}
+```
+
+**Requirements:**
+- The converter class must have a no-arg constructor
+- The constructor will be made accessible if needed (for nested classes, etc.)
+- If instantiation fails, the test will fail with a descriptive error message
+
+### Class-Level vs Method-Level
+
+Method-level annotations override class-level annotations for the same 
property. Properties not specified in the method annotation inherit from the 
class annotation.
+
+```java
+@BctConfig(sortMaps=TriState.TRUE, beanConverter=MyConverter.class)
+class MyTest {
+    @Test
+    @BctConfig(sortMaps=TriState.FALSE, sortCollections=TriState.TRUE)
+    void testMethod() {
+        // sortMaps=false (from method, overrides class)
+        // sortCollections=true (from method)
+        // beanConverter=MyConverter (from class, inherited)
+    }
+    
+    @Test
+    void testAnotherMethod() {
+        // sortMaps=true (from class)
+        // sortCollections=UNSET (default, not set)
+        // beanConverter=MyConverter (from class)
+    }
+}
+```
+
+### Using Default Converter
+
+To explicitly use the default converter when a class-level converter is set, 
use `BasicBeanConverter.class`:
+
+```java
+@BctConfig(beanConverter=MyConverter.class)
+class MyTest {
+    @Test
+    @BctConfig(beanConverter=BasicBeanConverter.class)
+    void testWithDefaultConverter() {
+        // Uses default converter, not MyConverter
+    }
+}
+```
+
+## BctConfiguration Class
+
+The `BctConfiguration` class provides a programmatic API for configuring BCT 
settings. It's useful when you need dynamic configuration or want to set up 
converters in test setup methods.
+
+### Setting Configuration Values
+
+```java
+import static org.apache.juneau.junit.bct.BctConfiguration.*;
+
+@BeforeEach
+void setUp() {
+    // Enable map sorting
+    set(BCT_SORT_MAPS, true);
+    
+    // Enable collection sorting
+    set(BCT_SORT_COLLECTIONS, true);
+    
+    // Set custom converter
+    var converter = BasicBeanConverter.builder()
+        .defaultSettings()
+        .addStringifier(LocalDate.class, date -> 
+            date.format(DateTimeFormatter.ISO_LOCAL_DATE))
+        .build();
+    set(converter);
+}
+
+@AfterEach
+void tearDown() {
+    // Clear all thread-local settings
+    clear();
+}
+```
+
+### Getting Configuration Values
+
+```java
+// Get a setting with a default value
+boolean sortMaps = BctConfiguration.get(BCT_SORT_MAPS, false);
+
+// Get a setting object (can check if set)
+StringSetting setting = BctConfiguration.get(BCT_SORT_MAPS);
+if (setting != null && setting.asBoolean()) {
+    // Map sorting is enabled
+}
+```
+
+### Global vs Thread-Local Settings
+
+Settings can be set globally (affecting all threads) or thread-locally 
(affecting only the current thread). Thread-local settings take precedence.
+
+```java
+// Set global setting (affects all threads)
+BctConfiguration.setGlobal(BCT_SORT_MAPS, true);
+
+// Set thread-local setting (overrides global for this thread)
+BctConfiguration.set(BCT_SORT_MAPS, false);
+
+// Clear thread-local settings
+BctConfiguration.clear();
+
+// Clear global settings
+BctConfiguration.clearGlobal();
+```
+
+## Configuration Properties
+
+### BCT_SORT_MAPS
+
+Property name: `"Bct.sortMaps"`
+
+When enabled, map entries are sorted by key before comparison, ensuring 
consistent assertion results regardless of map implementation or insertion 
order.
+
+```java
+BctConfiguration.set(BctConfiguration.BCT_SORT_MAPS, true);
+```
+
+### BCT_SORT_COLLECTIONS
+
+Property name: `"Bct.sortCollections"`
+
+When enabled, collection elements are sorted before comparison, ensuring 
consistent assertion results regardless of collection implementation or 
insertion order.
+
+```java
+BctConfiguration.set(BctConfiguration.BCT_SORT_COLLECTIONS, true);
+```
+
+## Custom Bean Converters
+
+Bean converters control how objects are accessed and converted to strings for 
comparison. BCT uses `BasicBeanConverter.DEFAULT` by default, but you can 
provide custom converters for specialized needs.
+
+### Creating a Custom Converter
+
+```java
+public static class MyCustomConverter extends BasicBeanConverter {
+    public MyCustomConverter() {
+        super(BasicBeanConverter.builder()
+            .defaultSettings()
+            // Add custom stringifier for LocalDate
+            .addStringifier(LocalDate.class, date -> 
+                date.format(DateTimeFormatter.ISO_LOCAL_DATE))
+            // Add custom stringifier for Money
+            .addStringifier(Money.class, money -> 
+                money.getAmount().toPlainString())
+            .build());
+    }
+}
+```
+
+### Using via Annotation
+
+```java
+@BctConfig(beanConverter=MyCustomConverter.class)
+class MyTest {
+    @Test
+    void testWithCustomConverter() {
+        assertBean(order, "date,total", "2023-12-01,99.99");
+    }
+}
+```
+
+### Using via Programmatic API
+
+```java
+@BeforeEach
+void setUp() {
+    var converter = BasicBeanConverter.builder()
+        .defaultSettings()
+        .addStringifier(LocalDate.class, date -> 
+            date.format(DateTimeFormatter.ISO_LOCAL_DATE))
+        .build();
+    BctConfiguration.set(converter);
+}
+
+@AfterEach
+void tearDown() {
+    BctConfiguration.clear(); // Also clears converter
+}
+```
+
+### Converter Requirements
+
+- Must have a no-arg constructor
+- Constructor will be made accessible if needed (for nested classes, private 
constructors, etc.)
+- If instantiation fails, a `RuntimeException` is thrown with a descriptive 
message
+
+## Thread Safety
+
+All configuration in BCT is thread-safe and uses thread-local storage. This 
ensures that:
+
+- **Parallel test execution** doesn't interfere with each other's settings
+- **Each test thread** has its own isolated configuration
+- **Settings are automatically cleared** after each test when using 
`@BctConfig`
+
+### Thread-Local Storage
+
+```java
+// Thread 1
+BctConfiguration.set(BCT_SORT_MAPS, true);
+
+// Thread 2 (running in parallel)
+BctConfiguration.set(BCT_SORT_MAPS, false);
+
+// Each thread has its own setting - no interference
+```
+
+### Automatic Cleanup
+
+When using `@BctConfig`, settings are automatically cleared after each test 
method:
+
+```java
+@BctConfig(sortMaps=TriState.TRUE)
+class MyTest {
+    @Test
+    void test1() {
+        // sortMaps is enabled
+    }
+    
+    @Test
+    void test2() {
+        // sortMaps is still enabled (from class annotation)
+        // But if test1 had method-level annotation, it would be cleared
+    }
+}
+```
+
+## Advanced Customization Topics
+
+For more advanced customization options, see:
+
+- **[Stringifiers](07.03.01.Stringifiers.md)** - Define how objects are 
converted to strings for comparison
+- **[Listifiers](07.03.02.Listifiers.md)** - Convert collection-like objects 
to lists for testing
+- **[Swappers](07.03.03.Swappers.md)** - Transform objects before processing 
(unwrap Optional, Supplier, etc.)
+
+## See Also
+
+- [juneau-bct Basics](07.01.00.JuneauBctBasics.md) - Main BCT documentation
+- [Custom Error Messages](07.02.00.CustomErrorMessages.md) - Enhanced error 
reporting
+- [Property Extractors](07.01.04.PropertyExtractors.md) - Custom property 
access logic
+
diff --git a/juneau-docs/docs/topics/07.01.01.Stringifiers.md 
b/juneau-docs/docs/topics/07.03.01.Stringifiers.md
similarity index 95%
rename from juneau-docs/docs/topics/07.01.01.Stringifiers.md
rename to juneau-docs/docs/topics/07.03.01.Stringifiers.md
index ac587d3b58..dda0ec41eb 100644
--- a/juneau-docs/docs/topics/07.01.01.Stringifiers.md
+++ b/juneau-docs/docs/topics/07.03.01.Stringifiers.md
@@ -190,8 +190,9 @@ try {
 
 ## See Also
 
-- [Listifiers](07.01.02.Listifiers.md) - Converting collection-like objects to 
lists
-- [Swappers](07.01.03.Swappers.md) - Transforming objects before processing
-- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property 
access logic
 - [juneau-bct Basics](07.01.00.JuneauBctBasics.md) - Main BCT documentation
+- [Customization](07.03.00.Customization.md) - Configuration via @BctConfig 
and BctConfiguration
+  - [Listifiers](07.03.02.Listifiers.md) - Converting collection-like objects 
to lists
+  - [Swappers](07.03.03.Swappers.md) - Transforming objects before processing
+- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property 
access logic
 
diff --git a/juneau-docs/docs/topics/07.01.02.Listifiers.md 
b/juneau-docs/docs/topics/07.03.02.Listifiers.md
similarity index 96%
rename from juneau-docs/docs/topics/07.01.02.Listifiers.md
rename to juneau-docs/docs/topics/07.03.02.Listifiers.md
index 6f9de86b2d..d3bed0b58e 100644
--- a/juneau-docs/docs/topics/07.01.02.Listifiers.md
+++ b/juneau-docs/docs/topics/07.03.02.Listifiers.md
@@ -259,8 +259,9 @@ Listifier<DataSource> safeListifier = (conv, source) -> {
 
 ## See Also
 
-- [Stringifiers](07.01.01.Stringifiers.md) - Converting objects to strings
-- [Swappers](07.01.03.Swappers.md) - Transforming objects before processing
-- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property 
access logic
 - [juneau-bct Basics](07.01.00.JuneauBctBasics.md) - Main BCT documentation
+- [Customization](07.03.00.Customization.md) - Configuration via @BctConfig 
and BctConfiguration
+  - [Stringifiers](07.03.01.Stringifiers.md) - Converting objects to strings
+  - [Swappers](07.03.03.Swappers.md) - Transforming objects before processing
+- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property 
access logic
 
diff --git a/juneau-docs/docs/topics/07.01.03.Swappers.md 
b/juneau-docs/docs/topics/07.03.03.Swappers.md
similarity index 97%
rename from juneau-docs/docs/topics/07.01.03.Swappers.md
rename to juneau-docs/docs/topics/07.03.03.Swappers.md
index ee060bd61b..df5429e2b4 100644
--- a/juneau-docs/docs/topics/07.01.03.Swappers.md
+++ b/juneau-docs/docs/topics/07.03.03.Swappers.md
@@ -355,8 +355,9 @@ Swapper<Validation> validationSwapper = (conv, validation) 
-> {
 
 ## See Also
 
-- [Stringifiers](07.01.01.Stringifiers.md) - Converting objects to strings
-- [Listifiers](07.01.02.Listifiers.md) - Converting collection-like objects to 
lists
-- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property 
access logic
 - [juneau-bct Basics](07.01.00.JuneauBctBasics.md) - Main BCT documentation
+- [Customization](07.03.00.Customization.md) - Configuration via @BctConfig 
and BctConfiguration
+  - [Stringifiers](07.03.01.Stringifiers.md) - Converting objects to strings
+  - [Listifiers](07.03.02.Listifiers.md) - Converting collection-like objects 
to lists
+- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property 
access logic
 
diff --git a/juneau-docs/sidebars.ts b/juneau-docs/sidebars.ts
index f54e0c96f2..4013176709 100644
--- a/juneau-docs/sidebars.ts
+++ b/juneau-docs/sidebars.ts
@@ -863,29 +863,41 @@ const sidebars: SidebarsConfig = {
         },
         {
           type: 'doc',
-          id: 'topics/07.01.01.Stringifiers',
-          label: '7.1.1. Stringifiers',
+          id: 'topics/07.02.00.CustomErrorMessages',
+          label: '7.2. Custom Error Messages',
         },
         {
-          type: 'doc',
-          id: 'topics/07.01.02.Listifiers',
-          label: '7.1.2. Listifiers',
-        },
-        {
-          type: 'doc',
-          id: 'topics/07.01.03.Swappers',
-          label: '7.1.3. Swappers',
+          type: 'category',
+          label: '7.3. Customization',
+          collapsed: true,
+          items: [
+            {
+              type: 'doc',
+              id: 'topics/07.03.00.Customization',
+              label: '7.3. Customization',
+            },
+            {
+              type: 'doc',
+              id: 'topics/07.03.01.Stringifiers',
+              label: '7.3.1. Stringifiers',
+            },
+            {
+              type: 'doc',
+              id: 'topics/07.03.02.Listifiers',
+              label: '7.3.2. Listifiers',
+            },
+            {
+              type: 'doc',
+              id: 'topics/07.03.03.Swappers',
+              label: '7.3.3. Swappers',
+            },
+          ],
         },
         {
           type: 'doc',
           id: 'topics/07.01.04.PropertyExtractors',
           label: '7.1.4. Property Extractors',
         },
-        {
-          type: 'doc',
-          id: 'topics/07.01.05.CustomErrorMessages',
-          label: '7.1.5. Custom Error Messages',
-        },
       ],
     },
     {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/BeanSession_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/BeanSession_Test.java
index 7ab35555c5..657af8756b 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/BeanSession_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/BeanSession_Test.java
@@ -16,8 +16,6 @@
  */
 package org.apache.juneau;
 
-import org.junit.jupiter.api.*;
-
 class BeanSession_Test extends TestBase {
        // Test class for BeanSession
 }
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/HeaderInfo_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/HeaderInfo_Test.java
index 9f39d8fe08..fbfd02f2e3 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/HeaderInfo_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/HeaderInfo_Test.java
@@ -221,7 +221,7 @@ class HeaderInfo_Test extends TestBase {
                }
 
                @Test void c07_keySet() {
-                       assertList(TESTER.bean().keySet(), "allowEmptyValue", 
"allowReserved", "deprecated", "description", "examples", "explode", 
"required", "schema", "style", "x-example", "x1", "x2");
+                       assertList(TESTER.bean().keySet(), "allowEmptyValue", 
"allowReserved", "deprecated", "description", "examples", "explode", 
"required", "schema", "x-example", "style", "x1", "x2");
                }
 
                @Test void c08_get() {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Items_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Items_Test.java
index a9cd1c6c1d..cd43cb75ad 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Items_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Items_Test.java
@@ -271,7 +271,7 @@ class Items_Test extends TestBase {
                }
 
                @Test void c07_keySet() {
-                       assertList(TESTER.bean().keySet(), "additionalItems", 
"allOf", "collectionFormat", "default", "discriminator", "enum", "example", 
"exclusiveMaximum", "exclusiveMinimum", "externalDocs", "format", "items", 
"maxItems", "maxLength", "maxProperties", "maximum", "minItems", "minLength", 
"minProperties", "minimum", "multipleOf", "pattern", "properties", "readOnly", 
"required", "title", "type", "uniqueItems", "x1", "x2", "xml");
+                       assertList(TESTER.bean().keySet(), "collectionFormat", 
"default", "enum", "exclusiveMaximum", "exclusiveMinimum", "format", "items", 
"maxItems", "maxLength", "maximum", "minItems", "minLength", "minimum", 
"multipleOf", "pattern", "type", "uniqueItems", "additionalItems", "allOf", 
"discriminator", "example", "externalDocs", "maxProperties", "minProperties", 
"properties", "readOnly", "required", "title", "xml", "x1", "x2");
                }
 
                @Test void c08_get() {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Parameter_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Parameter_Test.java
index 6f6b65acf4..e6ccd983f6 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Parameter_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Parameter_Test.java
@@ -228,7 +228,7 @@ class Parameter_Test extends TestBase {
                }
 
                @Test void c07_keySet() {
-                       assertList(TESTER.bean().keySet(), "allowEmptyValue", 
"allowReserved", "content", "deprecated", "description", "example", "examples", 
"explode", "in", "name", "required", "schema", "style", "x1", "x2");
+                       assertList(TESTER.bean().keySet(), "allowEmptyValue", 
"allowReserved", "deprecated", "description", "example", "examples", "explode", 
"in", "name", "required", "schema", "style", "content", "x1", "x2");
                }
 
                @Test void c08_get() {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/SchemaInfo_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/SchemaInfo_Test.java
index 7e99125e2e..62bc12efc6 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/SchemaInfo_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/SchemaInfo_Test.java
@@ -277,7 +277,7 @@ class SchemaInfo_Test extends TestBase {
                }
 
                @Test void c07_keySet() {
-                       assertList(TESTER.bean().keySet(), "additionalItems", 
"additionalProperties", "allOf", "anyOf", "default", "description", 
"discriminator", "enum", "example", "exclusiveMaximum", "exclusiveMinimum", 
"externalDocs", "format", "items", "maxItems", "maxLength", "maxProperties", 
"maximum", "minItems", "minLength", "minProperties", "minimum", "multipleOf", 
"not", "nullable", "oneOf", "pattern", "properties", "readOnly", "required", 
"title", "type", "uniqueItems", "writeOnly", "x1", "x2", "xml");
+                       assertList(TESTER.bean().keySet(), 
"additionalProperties", "allOf", "anyOf", "default", "description", 
"discriminator", "enum", "example", "exclusiveMaximum", "exclusiveMinimum", 
"externalDocs", "format", "items", "maxItems", "maxLength", "maxProperties", 
"maximum", "minItems", "minLength", "minProperties", "minimum", "multipleOf", 
"not", "nullable", "oneOf", "pattern", "properties", "readOnly", "required", 
"title", "type", "uniqueItems", "writeOnly", "xml", "additionalItems", "x1", 
"x2");
                }
 
                @Test void c08_get() {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/bean/swagger/SchemaInfo_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/bean/swagger/SchemaInfo_Test.java
index b4c92e38f1..b4ac43d8c0 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/bean/swagger/SchemaInfo_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/bean/swagger/SchemaInfo_Test.java
@@ -306,7 +306,7 @@ class SchemaInfo_Test extends TestBase {
                }
 
                @Test void c07_keySet() {
-                       assertList(TESTER.bean().keySet(), "$ref", 
"additionalProperties", "allOf", "default", "description", "discriminator", 
"enum", "example", "exclusiveMaximum", "exclusiveMinimum", "externalDocs", 
"format", "items", "maxItems", "maxLength", "maxProperties", "maximum", 
"minItems", "minLength", "minProperties", "minimum", "multipleOf", "pattern", 
"properties", "readOnly", "required", "requiredProperties", "title", "type", 
"uniqueItems", "x1", "x2", "xml");
+                       assertList(TESTER.bean().keySet(), "$ref", 
"additionalProperties", "allOf", "default", "description", "discriminator", 
"enum", "example", "exclusiveMaximum", "exclusiveMinimum", "externalDocs", 
"format", "items", "maxItems", "maxLength", "maxProperties", "maximum", 
"minItems", "minLength", "minProperties", "minimum", "multipleOf", "pattern", 
"properties", "readOnly", "required", "requiredProperties", "title", "type", 
"uniqueItems", "xml", "x1", "x2");
                }
 
                @Test void c08_get() {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Maps_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Maps_Test.java
index 150139c615..47478b3c68 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Maps_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Maps_Test.java
@@ -16,12 +16,14 @@
  */
 package org.apache.juneau.commons.collections;
 
+import static org.apache.juneau.commons.lang.TriState.*;
 import static org.apache.juneau.junit.bct.BctAssertions.*;
 import static org.junit.jupiter.api.Assertions.*;
 
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.junit.bct.annotations.*;
 import org.junit.jupiter.api.*;
 
 class Maps_Test extends TestBase {
@@ -92,6 +94,7 @@ class Maps_Test extends TestBase {
        
//-----------------------------------------------------------------------------------------------------------------
 
        @Test
+       @BctConfig(sortMaps = TRUE)
        void b01_addPairs() {
                var map = Maps.create(String.class, String.class)
                        .addPairs("host", "localhost", "port", "8080", 
"protocol", "https")
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Sets_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Sets_Test.java
index 4d6db8e676..b394fcab34 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Sets_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Sets_Test.java
@@ -16,6 +16,7 @@
  */
 package org.apache.juneau.commons.collections;
 
+import static org.apache.juneau.commons.lang.TriState.*;
 import static org.apache.juneau.commons.utils.CollectionUtils.*;
 import static org.apache.juneau.junit.bct.BctAssertions.*;
 import static org.junit.jupiter.api.Assertions.*;
@@ -23,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.*;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.junit.bct.annotations.*;
 import org.junit.jupiter.api.*;
 
 class Sets_Test extends TestBase {
@@ -849,6 +851,7 @@ class Sets_Test extends TestBase {
        }
 
        @Test
+       @BctConfig(sortCollections = TRUE)
        void u02_filtered_multipleFilters() {
                var set = Sets.create(Integer.class)
                        .filtered(v -> v != null)
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BasicBeanConverter_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BasicBeanConverter_Test.java
index 2409c601ba..a3e9d6aa32 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BasicBeanConverter_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BasicBeanConverter_Test.java
@@ -16,6 +16,7 @@
  */
 package org.apache.juneau.junit.bct;
 
+import static org.apache.juneau.commons.lang.TriState.*;
 import static org.apache.juneau.commons.utils.CollectionUtils.*;
 import static org.apache.juneau.commons.utils.Utils.*;
 import static org.apache.juneau.junit.bct.BasicBeanConverter.*;
@@ -29,6 +30,7 @@ import java.util.*;
 import java.util.concurrent.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.junit.bct.annotations.*;
 import org.junit.jupiter.api.*;
 
 /**
@@ -172,6 +174,7 @@ class BasicBeanConverter_Test extends TestBase {
 
                @Test
                @DisplayName("b04_stringify() handles collections")
+               @BctConfig(sortCollections = TRUE)
                void b04_stringify_handlesCollections() {
                        assertEquals("[1,2,3]", converter.stringify(l(1, 2, 
3)));
                        assertEquals("[]", 
converter.stringify(Collections.emptyList()));
@@ -216,6 +219,7 @@ class BasicBeanConverter_Test extends TestBase {
 
                @Test
                @DisplayName("b09_listify() handles collections")
+               @BctConfig(sortCollections = TRUE)
                void b09_listify_handlesCollections() {
                        var set = Set.of("z", "a", "m");
                        var result = converter.listify(set);
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctAssertions_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctAssertions_Test.java
index 92961f8778..cff53d2307 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctAssertions_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctAssertions_Test.java
@@ -20,12 +20,14 @@ import static 
org.apache.juneau.commons.utils.CollectionUtils.*;
 import static org.apache.juneau.commons.utils.Utils.*;
 import static org.apache.juneau.junit.bct.BctAssertions.*;
 import static org.junit.jupiter.api.Assertions.*;
+import static org.apache.juneau.commons.lang.TriState.*;
 
 import java.util.*;
 import java.util.function.*;
 import java.util.stream.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.junit.bct.annotations.*;
 import org.junit.jupiter.api.*;
 import org.opentest4j.*;
 
@@ -392,6 +394,7 @@ class BctAssertions_Test extends TestBase {
        // 
====================================================================================================
 
        @Nested
+       @BctConfig(sortMaps = TRUE)
        class H_assertMap extends TestBase {
 
                @Test
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctConfig_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctConfig_Test.java
new file mode 100644
index 0000000000..2a3dbaa560
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctConfig_Test.java
@@ -0,0 +1,306 @@
+/*
+ * 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 "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.bct;
+
+import static org.apache.juneau.commons.lang.TriState.*;
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.apache.juneau.junit.bct.BctConfiguration.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.junit.bct.annotations.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Unit tests for {@link BctConfig} annotation and extension.
+ */
+class BctConfig_Test extends TestBase {
+
+       // 
====================================================================================================
+       // Class-level annotation tests
+       // 
====================================================================================================
+
+       @Nested
+       @BctConfig(sortMaps = TRUE)
+       class A_classLevelAnnotation extends TestBase {
+
+               @Test
+               void a01_sortMapsEnabled() {
+                       // Verify sortMaps is enabled from class-level 
annotation
+                       assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+               }
+
+               @Test
+               void a02_sortCollectionsNotEnabled() {
+                       // Verify sortCollections is not enabled (default UNSET)
+                       assertFalse(BctConfiguration.get(BCT_SORT_COLLECTIONS, 
false));
+               }
+       }
+
+       @Nested
+       @BctConfig(sortMaps = TRUE, sortCollections = TRUE)
+       class B_classLevelBothEnabled extends TestBase {
+
+               @Test
+               void b01_bothEnabled() {
+                       // Verify both are enabled from class-level annotation
+                       assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+                       assertTrue(BctConfiguration.get(BCT_SORT_COLLECTIONS, 
false));
+               }
+       }
+
+       // 
====================================================================================================
+       // Method-level annotation tests
+       // 
====================================================================================================
+
+       @Nested
+       class C_methodLevelAnnotation extends TestBase {
+
+               @Test
+               @BctConfig(sortMaps = TRUE)
+               void c01_sortMapsEnabled() {
+                       // Verify sortMaps is enabled from method-level 
annotation
+                       assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+               }
+
+               @Test
+               @BctConfig(sortCollections = TRUE)
+               void c02_sortCollectionsEnabled() {
+                       // Verify sortCollections is enabled from method-level 
annotation
+                       assertTrue(BctConfiguration.get(BCT_SORT_COLLECTIONS, 
false));
+               }
+
+               @Test
+               void c03_noAnnotation() {
+                       // Verify no settings are enabled when no annotation is 
present
+                       assertFalse(BctConfiguration.get(BCT_SORT_MAPS, false));
+                       assertFalse(BctConfiguration.get(BCT_SORT_COLLECTIONS, 
false));
+               }
+       }
+
+       // 
====================================================================================================
+       // Method-level overriding class-level tests
+       // 
====================================================================================================
+
+       @Nested
+       @BctConfig(sortMaps = TRUE, sortCollections = TRUE)
+       class D_methodOverridesClass extends TestBase {
+
+               @Test
+               @BctConfig(sortMaps = FALSE)
+               void d01_methodOverridesSortMaps() {
+                       // Method-level should override class-level for sortMaps
+                       assertFalse(BctConfiguration.get(BCT_SORT_MAPS, false));
+                       // sortCollections should still be true from 
class-level (UNSET in method inherits from class)
+                       assertTrue(BctConfiguration.get(BCT_SORT_COLLECTIONS, 
false));
+               }
+
+               @Test
+               @BctConfig(sortCollections = FALSE)
+               void d02_methodOverridesSortCollections() {
+                       // sortMaps should still be true from class-level 
(UNSET in method inherits from class)
+                       assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+                       // Method-level should override class-level for 
sortCollections
+                       assertFalse(BctConfiguration.get(BCT_SORT_COLLECTIONS, 
false));
+               }
+
+               @Test
+               @BctConfig(sortMaps = FALSE, sortCollections = FALSE)
+               void d03_methodOverridesBoth() {
+                       // Method-level should override both from class-level
+                       assertFalse(BctConfiguration.get(BCT_SORT_MAPS, false));
+                       assertFalse(BctConfiguration.get(BCT_SORT_COLLECTIONS, 
false));
+               }
+
+               @Test
+               void d04_inheritsFromClass() {
+                       // No method annotation, should inherit from class
+                       assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+                       assertTrue(BctConfiguration.get(BCT_SORT_COLLECTIONS, 
false));
+               }
+       }
+
+       // 
====================================================================================================
+       // BeanConverter annotation tests
+       // 
====================================================================================================
+
+       /**
+        * Custom converter for testing.
+        */
+       static class TestBeanConverter extends BasicBeanConverter {
+               public TestBeanConverter() {
+                       super(BasicBeanConverter.builder().defaultSettings());
+               }
+       }
+
+       @Nested
+       class E_beanConverterAnnotation extends TestBase {
+
+               @Test
+               @BctConfig(beanConverter = TestBeanConverter.class)
+               void e01_customConverterSet() {
+                       // Verify custom converter is set by using it in an 
assertion
+                       // The converter should be used by assertBean
+                       var person = new TestPerson("Alice", 25);
+                       assertDoesNotThrow(() -> assertBean(person, "name", 
"Alice"));
+                       // Verify it's actually the custom converter by 
checking the class
+                       var converter = BctConfiguration.getConverter();
+                       assertNotNull(converter);
+                       assertEquals(TestBeanConverter.class, 
converter.getClass());
+               }
+
+               @Test
+               void e02_defaultConverter() {
+                       // Verify default converter is used when no annotation
+                       var converter = BctConfiguration.getConverter();
+                       assertNotNull(converter);
+                       assertEquals(BasicBeanConverter.class, 
converter.getClass());
+               }
+       }
+
+       @Nested
+       @BctConfig(beanConverter = TestBeanConverter.class)
+       class F_classLevelBeanConverter extends TestBase {
+
+               @Test
+               void f01_inheritsConverterFromClass() {
+                       // Verify converter is inherited from class-level 
annotation
+                       var converter = BctConfiguration.getConverter();
+                       assertNotNull(converter);
+                       assertEquals(TestBeanConverter.class, 
converter.getClass());
+               }
+
+               @Test
+               @BctConfig(beanConverter = BeanConverter.class)
+               void f02_methodFallsBackToClassConverter() {
+                       // Method-level with BeanConverter.class (default) 
falls back to class-level converter
+                       var converter = BctConfiguration.getConverter();
+                       assertNotNull(converter);
+                       assertEquals(TestBeanConverter.class, 
converter.getClass());
+               }
+
+               @Test
+               @BctConfig(beanConverter = BasicBeanConverter.class)
+               void f03_methodExplicitlyUsesDefaultConverter() {
+                       // Method-level with BasicBeanConverter.class 
explicitly uses default converter,
+                       // overriding class-level TestBeanConverter
+                       var converter = BctConfiguration.getConverter();
+                       assertNotNull(converter);
+                       assertEquals(BasicBeanConverter.class, 
converter.getClass());
+               }
+       }
+
+       // 
====================================================================================================
+       // Combined settings tests
+       // 
====================================================================================================
+
+       @Nested
+       @BctConfig(sortMaps = TRUE, beanConverter = TestBeanConverter.class)
+       class G_combinedSettings extends TestBase {
+
+               @Test
+               void g01_bothSettingsApplied() {
+                       // Verify both sortMaps and converter are set
+                       assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+                       var converter = BctConfiguration.getConverter();
+                       assertEquals(TestBeanConverter.class, 
converter.getClass());
+               }
+
+               @Test
+               @BctConfig(sortCollections = TRUE)
+               void g02_methodAddsToClass() {
+                       // Method adds sortCollections, inherits sortMaps and 
converter from class
+                       assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+                       assertTrue(BctConfiguration.get(BCT_SORT_COLLECTIONS, 
false));
+                       var converter = BctConfiguration.getConverter();
+                       assertEquals(TestBeanConverter.class, 
converter.getClass());
+               }
+       }
+
+       // 
====================================================================================================
+       // Clearing after test tests
+       // 
====================================================================================================
+
+       @Nested
+       class H_clearingAfterTest extends TestBase {
+
+               @Test
+               @BctConfig(sortMaps = TRUE, sortCollections = TRUE)
+               void h01_settingsClearedAfterTest() {
+                       // Settings should be active during test
+                       assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+                       assertTrue(BctConfiguration.get(BCT_SORT_COLLECTIONS, 
false));
+               }
+
+               @AfterEach
+               void verifyCleared() {
+                       // After test, settings should be cleared
+                       // Note: This runs after the extension's afterEach, so 
we need to check in the next test
+               }
+
+               @Test
+               void h02_verifyPreviousTestCleared() {
+                       // This test runs after h01, so settings should be 
cleared
+                       assertFalse(BctConfiguration.get(BCT_SORT_MAPS, false));
+                       assertFalse(BctConfiguration.get(BCT_SORT_COLLECTIONS, 
false));
+               }
+       }
+
+       // 
====================================================================================================
+       // Error handling tests
+       // 
====================================================================================================
+
+       @Nested
+       class I_errorHandling extends TestBase {
+
+               /**
+                * Converter without no-arg constructor (should fail).
+                */
+               static class InvalidConverter extends BasicBeanConverter {
+                       public InvalidConverter(String arg) {
+                               
super(BasicBeanConverter.builder().defaultSettings());
+                       }
+               }
+       }
+
+       // 
====================================================================================================
+       // Test helper classes
+       // 
====================================================================================================
+
+       /**
+        * Test person class for testing.
+        */
+       static class TestPerson {
+               private final String name;
+               private final int age;
+
+               TestPerson(String name, int age) {
+                       this.name = name;
+                       this.age = age;
+               }
+
+               String getName() { return name; }
+
+               int getAge() { return age; }
+
+               @Override
+               public String toString() {
+                       return "TestPerson{name='" + name + "', age=" + age + 
"}";
+               }
+       }
+}
+
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/Listifiers_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/Listifiers_Test.java
index 7088d5454f..811625c79b 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/Listifiers_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/Listifiers_Test.java
@@ -16,6 +16,7 @@
  */
 package org.apache.juneau.junit.bct;
 
+import static org.apache.juneau.commons.lang.TriState.*;
 import static org.apache.juneau.commons.utils.CollectionUtils.*;
 import static org.apache.juneau.commons.utils.Utils.*;
 import static org.apache.juneau.junit.bct.BctAssertions.*;
@@ -25,6 +26,7 @@ import java.util.*;
 import java.util.stream.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.junit.bct.annotations.*;
 import org.junit.jupiter.api.*;
 
 /**
@@ -46,20 +48,21 @@ class Listifiers_Test extends TestBase {
                }
 
                @Test
+               @BctConfig(sortCollections = TRUE)
                void a02_listifySet() {
                        var listifier = Listifiers.collectionListifier();
                        var input = Set.of("z", "a", "m"); // Unordered input
                        var result = listifier.apply(null, input);
-
-                       // TreeSet conversion ensures natural ordering
                        assertList(result, "a", "m", "z");
                }
 
                @Test
+               @BctConfig(sortCollections = TRUE)
                void a02a_listifySetTypes() {
                        var listifier = Listifiers.collectionListifier();
 
                        // HashSet (unordered) -> converted to TreeSet for 
natural ordering
+                       BctConfiguration.set(BctConfiguration.BCT_SORT_MAPS, 
true);
                        var hashSet = new HashSet<>(l("z", "a", "m"));
                        var hashResult = listifier.apply(null, hashSet);
                        assertList(hashResult, "a", "m", "z");
@@ -298,6 +301,7 @@ class Listifiers_Test extends TestBase {
        class F_mapListifier extends TestBase {
 
                @Test
+               @BctConfig(sortMaps = TRUE)
                void f01_listifyMap() {
                        var listifier = Listifiers.mapListifier();
                        var input = m("z", "value1", "a", "value2"); // 
Unordered input
@@ -314,6 +318,7 @@ class Listifiers_Test extends TestBase {
                }
 
                @Test
+               @BctConfig(sortMaps = TRUE)
                void f01a_listifyMapTypes() {
                        var listifier = Listifiers.mapListifier();
 


Reply via email to