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 48a5b03893 Unit tests
48a5b03893 is described below
commit 48a5b038939b18626dc2d53cb64283f1adbf2f9c
Author: James Bognar <[email protected]>
AuthorDate: Mon Dec 1 09:21:12 2025 -0800
Unit tests
---
BCT_CONVERTER_OVERRIDE_FEASIBILITY.md | 439 ------------
TODO.md | 2 +-
.../org/apache/juneau/junit/bct/AssertionArgs.java | 265 --------
.../org/apache/juneau/junit/bct/BctAssertions.java | 735 +++++++++++++--------
.../org/apache/juneau/junit/bct/package-info.java | 79 ++-
.../docs/topics/07.01.00.JuneauBctBasics.md | 40 +-
juneau-docs/docs/topics/07.01.01.Stringifiers.md | 16 +-
juneau-docs/docs/topics/07.01.02.Listifiers.md | 32 +-
juneau-docs/docs/topics/07.01.03.Swappers.md | 34 +-
.../docs/topics/07.01.04.PropertyExtractors.md | 74 ++-
.../docs/topics/07.01.05.CustomErrorMessages.md | 117 ++--
.../juneau/junit/bct/AssertionArgs_Test.java | 526 ---------------
.../juneau/junit/bct/BctAssertions_Test.java | 105 +--
13 files changed, 731 insertions(+), 1733 deletions(-)
diff --git a/BCT_CONVERTER_OVERRIDE_FEASIBILITY.md
b/BCT_CONVERTER_OVERRIDE_FEASIBILITY.md
deleted file mode 100644
index 3531379792..0000000000
--- a/BCT_CONVERTER_OVERRIDE_FEASIBILITY.md
+++ /dev/null
@@ -1,439 +0,0 @@
-# Feasibility Analysis: Overriding BctAssertions.DEFAULT_CONVERTER
-
-## Overview
-
-This document analyzes the feasibility of making
`BctAssertions.DEFAULT_CONVERTER` a resettable, memoized thread-local field
that can be overridden during test setup.
-
-## Current State
-
-- `DEFAULT_CONVERTER` is currently a `static final` field:
`BasicBeanConverter.DEFAULT`
-- Used throughout `BctAssertions` via:
`args.getBeanConverter().orElse(DEFAULT_CONVERTER)`
-- Custom converters can be provided per-assertion via
`AssertionArgs.setBeanConverter()`
-- TODO-88 exists to eliminate the need for `AssertionArgs` by making the
default converter resettable
-
-## Proposed Solution
-
-### Core Implementation
-
-```java
-public class BctAssertions {
- // Thread-local with memoized default
- private static final ThreadLocal<ResettableSupplier<BeanConverter>>
DEFAULT_CONVERTER =
- ThreadLocal.withInitial(() -> memoizeResettable(() ->
BasicBeanConverter.DEFAULT));
-
- // Get the current converter for this thread
- private static BeanConverter getDefaultConverter() {
- return DEFAULT_CONVERTER.get().get();
- }
-
- // Override the converter for this thread
- public static void setDefaultConverter(BeanConverter converter) {
- DEFAULT_CONVERTER.get().reset();
- // Need to set a new value - see implementation options below
- }
-
- // Reset to default for this thread
- public static void resetDefaultConverter() {
- DEFAULT_CONVERTER.get().reset();
- }
-}
-```
-
-## JUnit 5 Parallel Execution Considerations
-
-### How JUnit 5 Parallelization Works
-
-1. **Test Method Parallelization**: When
`junit.jupiter.execution.parallel.mode.default = concurrent`, each test method
can run in its own thread
-2. **Test Class Parallelization**: When
`junit.jupiter.execution.parallel.mode.classes.default = concurrent`, different
test classes run in parallel
-3. **Thread Isolation**: Each test method thread has its own `ThreadLocal`
storage
-
-### Thread-Local Behavior
-
-✅ **Advantages:**
-- Each parallel test method gets its own converter instance
-- No cross-thread interference
-- Thread-safe by design
-
-⚠️ **Challenges:**
-- Tests in the same class running in parallel will have **separate** converter
instances
-- Cannot easily share a converter across all tests in a class when running in
parallel
-
-## Options for Class-Level Converter Sharing
-
-### Option 1: Thread-Local Only (Method-Level Isolation)
-
-**Behavior:**
-- Each test method gets its own converter instance
-- Tests in the same class running in parallel use different converters
-- `@BeforeEach` can set converter per test method
-- `@AfterEach` can reset converter per test method
-
-**Pros:**
-- Simple implementation
-- No cross-test interference
-- Works perfectly with parallel execution
-
-**Cons:**
-- Cannot share converter across all tests in a class when running in parallel
-- Requires setting converter in each test method if you want customization
-
-**Use Case:**
-```java
-@BeforeEach
-void setUp() {
- var customConverter = BasicBeanConverter.builder()
- .defaultSettings()
- .addStringifier(MyType.class, obj -> obj.customFormat())
- .build();
- BctAssertions.setDefaultConverter(customConverter);
-}
-
-@AfterEach
-void tearDown() {
- BctAssertions.resetDefaultConverter();
-}
-```
-
-### Option 2: Class-Level Thread-Local with Synchronization
-
-**Behavior:**
-- Use a `ConcurrentHashMap<Class<?>, BeanConverter>` to store class-level
converters
-- Thread-local checks class-level first, then falls back to thread-local
-- Requires synchronization or atomic operations
-
-**Implementation Sketch:**
-```java
-private static final ConcurrentHashMap<Class<?>, BeanConverter>
CLASS_CONVERTERS = new ConcurrentHashMap<>();
-private static final ThreadLocal<ResettableSupplier<BeanConverter>>
THREAD_CONVERTER =
- ThreadLocal.withInitial(() -> memoizeResettable(() -> {
- // Check class-level first
- Class<?> testClass = findTestClass(); // Need to detect current test
class
- BeanConverter classConverter = CLASS_CONVERTERS.get(testClass);
- return classConverter != null ? classConverter :
BasicBeanConverter.DEFAULT;
- }));
-
-public static void setDefaultConverterForClass(Class<?> testClass,
BeanConverter converter) {
- CLASS_CONVERTERS.put(testClass, converter);
- // Invalidate thread-local cache for all threads running this class
- THREAD_CONVERTER.get().reset();
-}
-```
-
-**Pros:**
-- Can share converter across all tests in a class
-- Still thread-safe
-
-**Cons:**
-- Complex implementation
-- Requires detecting current test class (reflection/stack trace)
-- Thread-local cache invalidation is tricky
-- May not work well with nested test classes
-
-### Option 3: Hybrid Approach with Test Class Detection
-
-**Behavior:**
-- Use `ThreadLocal` with a `ResettableSupplier` that checks for class-level
overrides
-- Detect test class from stack trace or via explicit registration
-- Cache converter per thread, but check class-level map on cache miss
-
-**Implementation Sketch:**
-```java
-private static final ConcurrentHashMap<Class<?>, BeanConverter>
CLASS_CONVERTERS = new ConcurrentHashMap<>();
-private static final ThreadLocal<ResettableSupplier<BeanConverter>>
THREAD_CONVERTER =
- ThreadLocal.withInitial(() -> memoizeResettable(() -> {
- Class<?> testClass = findTestClassFromStack();
- return CLASS_CONVERTERS.getOrDefault(testClass,
BasicBeanConverter.DEFAULT);
- }));
-
-private static Class<?> findTestClassFromStack() {
- StackTraceElement[] stack = Thread.currentThread().getStackTrace();
- for (StackTraceElement element : stack) {
- try {
- Class<?> clazz = Class.forName(element.getClassName());
- if (clazz.getName().endsWith("_Test") ||
clazz.isAnnotationPresent(TestClass.class)) {
- return clazz;
- }
- } catch (ClassNotFoundException e) {
- // Continue
- }
- }
- return null;
-}
-```
-
-**Pros:**
-- Automatic class detection
-- Supports both class-level and method-level overrides
-- Thread-safe
-
-**Cons:**
-- Stack trace inspection is expensive (but memoized)
-- May incorrectly detect class in some scenarios
-- Complex implementation
-
-### Option 4: Explicit Test Class Registration (Recommended)
-
-**Behavior:**
-- Provide explicit methods for class-level and method-level overrides
-- Use `@BeforeAll` to set class-level converter
-- Use `@BeforeEach` to set method-level converter
-- Thread-local stores the active converter
-
-**Implementation:**
-```java
-public class BctAssertions {
- // Class-level converters (shared across all threads for a class)
- private static final ConcurrentHashMap<Class<?>, BeanConverter>
CLASS_CONVERTERS = new ConcurrentHashMap<>();
-
- // Thread-local converter (method-level override)
- private static final ThreadLocal<ResettableSupplier<BeanConverter>>
THREAD_CONVERTER =
- ThreadLocal.withInitial(() -> memoizeResettable(() -> {
- // Check if current thread has a class-level converter
- Class<?> testClass = getTestClassForThread();
- if (testClass != null) {
- BeanConverter classConverter = CLASS_CONVERTERS.get(testClass);
- if (classConverter != null) {
- return classConverter;
- }
- }
- return BasicBeanConverter.DEFAULT;
- }));
-
- // Set converter for all tests in a class
- public static void setDefaultConverterForClass(Class<?> testClass,
BeanConverter converter) {
- CLASS_CONVERTERS.put(testClass, converter);
- // Note: Existing threads will continue using their cached value
- // New threads will pick up the class-level converter
- }
-
- // Set converter for current thread (method-level)
- public static void setDefaultConverter(BeanConverter converter) {
- // Store in a separate thread-local for method-level override
- METHOD_CONVERTER.set(converter);
- THREAD_CONVERTER.get().reset(); // Force recomputation
- }
-
- private static BeanConverter getDefaultConverter() {
- // Check method-level first, then class-level, then default
- BeanConverter methodConverter = METHOD_CONVERTER.get();
- if (methodConverter != null) {
- return methodConverter;
- }
- return THREAD_CONVERTER.get().get();
- }
-}
-```
-
-**Usage:**
-```java
-public class MyTest extends TestBase {
- @BeforeAll
- static void setUpClass() {
- var classConverter = BasicBeanConverter.builder()
- .defaultSettings()
- .addStringifier(MyType.class, obj -> obj.customFormat())
- .build();
- BctAssertions.setDefaultConverterForClass(MyTest.class,
classConverter);
- }
-
- @AfterAll
- static void tearDownClass() {
- BctAssertions.clearDefaultConverterForClass(MyTest.class);
- }
-
- @BeforeEach
- void setUp() {
- // Optional: Override for this specific test method
- // BctAssertions.setDefaultConverter(customConverter);
- }
-
- @AfterEach
- void tearDown() {
- // Optional: Reset method-level override
- // BctAssertions.resetDefaultConverter();
- }
-}
-```
-
-**Pros:**
-- Clear and explicit API
-- Supports both class-level and method-level overrides
-- Works with parallel execution
-- No stack trace inspection needed
-- Easy to understand and maintain
-
-**Cons:**
-- Requires explicit class registration in `@BeforeAll`
-- Class-level converters persist until explicitly cleared (but that's usually
desired)
-
-## Recommended Implementation
-
-I recommend **Option 4 (Explicit Test Class Registration)** because:
-
-1. **Clarity**: Explicit is better than implicit - developers know exactly
what's happening
-2. **Performance**: No stack trace inspection overhead
-3. **Flexibility**: Supports both class-level and method-level overrides
-4. **Parallel-Safe**: Works correctly with JUnit 5 parallel execution
-5. **Maintainability**: Simple to understand and debug
-
-### Implementation Details
-
-```java
-public class BctAssertions {
- // Class-level converters (shared across threads for a class)
- private static final ConcurrentHashMap<Class<?>, BeanConverter>
CLASS_CONVERTERS = new ConcurrentHashMap<>();
-
- // Method-level converter override (thread-local)
- private static final ThreadLocal<BeanConverter> METHOD_CONVERTER = new
ThreadLocal<>();
-
- // Thread-local memoized supplier for class-level or default converter
- private static final ThreadLocal<ResettableSupplier<BeanConverter>>
THREAD_CONVERTER =
- ThreadLocal.withInitial(() -> memoizeResettable(() -> {
- // Find the test class for this thread
- Class<?> testClass = findTestClassForThread();
- if (testClass != null) {
- BeanConverter classConverter = CLASS_CONVERTERS.get(testClass);
- if (classConverter != null) {
- return classConverter;
- }
- }
- return BasicBeanConverter.DEFAULT;
- }));
-
- // Internal: Get converter (checks method-level first, then
class-level/default)
- private static BeanConverter getDefaultConverter() {
- BeanConverter methodConverter = METHOD_CONVERTER.get();
- if (methodConverter != null) {
- return methodConverter;
- }
- return THREAD_CONVERTER.get().get();
- }
-
- // Public API: Set converter for all tests in a class
- public static void setDefaultConverterForClass(Class<?> testClass,
BeanConverter converter) {
- assertArgNotNull("testClass", testClass);
- assertArgNotNull("converter", converter);
- CLASS_CONVERTERS.put(testClass, converter);
- // Invalidate thread-local cache for this class's threads
- // Note: This is best-effort; existing threads may continue with
cached value
- // until they call getDefaultConverter() again
- }
-
- // Public API: Clear converter for a class
- public static void clearDefaultConverterForClass(Class<?> testClass) {
- assertArgNotNull("testClass", testClass);
- CLASS_CONVERTERS.remove(testClass);
- }
-
- // Public API: Set converter for current thread (method-level override)
- public static void setDefaultConverter(BeanConverter converter) {
- assertArgNotNull("converter", converter);
- METHOD_CONVERTER.set(converter);
- }
-
- // Public API: Reset converter for current thread (clears method-level
override)
- public static void resetDefaultConverter() {
- METHOD_CONVERTER.remove();
- THREAD_CONVERTER.get().reset(); // Also reset class-level cache
- }
-
- // Internal: Find test class for current thread (simplified - may need
refinement)
- private static Class<?> findTestClassForThread() {
- // This is a simplified version - you may want to cache this per thread
- // or use a more sophisticated detection mechanism
- StackTraceElement[] stack = Thread.currentThread().getStackTrace();
- for (StackTraceElement element : stack) {
- String className = element.getClassName();
- if (className.endsWith("_Test") || className.contains("Test")) {
- try {
- return Class.forName(className);
- } catch (ClassNotFoundException e) {
- // Continue
- }
- }
- }
- return null;
- }
-
- // Update all methods to use getDefaultConverter() instead of
DEFAULT_CONVERTER
- // Example:
- public static void assertBean(Object actual, String fields, String
expected) {
- assertBean(args(), actual, fields, expected);
- }
-
- public static void assertBean(AssertionArgs args, Object actual, String
fields, String expected) {
- // ... existing code ...
- var converter = args.getBeanConverter().orElse(getDefaultConverter());
- // ... rest of method ...
- }
-}
-```
-
-## Alternative: Simpler Thread-Local Only Approach
-
-If class-level sharing is not a requirement, a simpler implementation is:
-
-```java
-public class BctAssertions {
- private static final ThreadLocal<ResettableSupplier<BeanConverter>>
DEFAULT_CONVERTER =
- ThreadLocal.withInitial(() -> memoizeResettable(() ->
BasicBeanConverter.DEFAULT));
-
- private static BeanConverter getDefaultConverter() {
- return DEFAULT_CONVERTER.get().get();
- }
-
- public static void setDefaultConverter(BeanConverter converter) {
- assertArgNotNull("converter", converter);
- // Store override in a separate thread-local
- CONVERTER_OVERRIDE.set(converter);
- DEFAULT_CONVERTER.get().reset(); // Invalidate cache
- }
-
- public static void resetDefaultConverter() {
- CONVERTER_OVERRIDE.remove();
- DEFAULT_CONVERTER.get().reset();
- }
-
- private static final ThreadLocal<BeanConverter> CONVERTER_OVERRIDE = new
ThreadLocal<>();
-
- private static BeanConverter getDefaultConverter() {
- BeanConverter override = CONVERTER_OVERRIDE.get();
- if (override != null) {
- return override;
- }
- return DEFAULT_CONVERTER.get().get();
- }
-}
-```
-
-This simpler approach:
-- ✅ Works perfectly with parallel execution
-- ✅ Each test method can set its own converter
-- ✅ No class-level sharing (each test is independent)
-- ✅ Much simpler implementation
-
-## Questions to Answer
-
-1. **Do you need class-level converter sharing?**
- - If YES → Use Option 4 (Explicit Registration)
- - If NO → Use simpler thread-local only approach
-
-2. **How should converter be set in test setup?**
- - `@BeforeAll` for class-level?
- - `@BeforeEach` for method-level?
- - Both?
-
-3. **Should converter persist across test methods in a class?**
- - If tests run sequentially → Yes, can share
- - If tests run in parallel → Each thread gets its own
-
-## Recommendation
-
-Start with the **simpler thread-local only approach** unless you have a
specific need for class-level sharing. You can always add class-level support
later if needed.
-
-The simpler approach:
-- Solves the core problem (eliminating need for AssertionArgs)
-- Works perfectly with parallel execution
-- Is easy to implement and maintain
-- Can be enhanced later if class-level sharing is needed
-
diff --git a/TODO.md b/TODO.md
index 3a1e5071e8..85ec6d7c7e 100644
--- a/TODO.md
+++ b/TODO.md
@@ -40,7 +40,7 @@ This file tracks pending tasks for the Apache Juneau project.
For completed item
- [ ] TODO-19 ClassInfo improvements to getMethod (e.g. getMethodExact vs
getMethod).
- [ ] TODO-21 Thrown NotFound causes - javax.servlet.ServletException: Invalid
method response: 200
-- [ ] TODO-88 Eliminate need for AssertionArgs in BctAssertions by allowing
DEFAULT_CONVERTER to be overridden and resettable.
+- [x] TODO-88 Eliminate need for AssertionArgs in BctAssertions by allowing
DEFAULT_CONVERTER to be overridden and resettable. ✅ COMPLETED - Implemented
thread-local converter with setConverter()/resetConverter() methods, removed
AssertionArgs class entirely, replaced with Supplier<String> for messages. All
tests updated and passing.
- [ ] TODO-89 Add ClassInfoTyped
## HTTP Response/Exception Improvements
diff --git
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/AssertionArgs.java
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/AssertionArgs.java
deleted file mode 100644
index fb40b59c9c..0000000000
---
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/AssertionArgs.java
+++ /dev/null
@@ -1,265 +0,0 @@
-/*
- * 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.common.utils.Utils.*;
-
-import java.text.*;
-import java.util.*;
-import java.util.function.*;
-
-/**
- * Configuration and context object for advanced assertion operations.
- *
- * <p>This class encapsulates additional arguments and configuration options
for assertion methods
- * in the Bean-Centric Testing (BCT) framework. It provides a fluent API for
customizing assertion
- * behavior including custom converters and enhanced error messaging.</p>
- *
- * <p>The primary purposes of this class are:</p>
- * <ul>
- * <li><b>Custom Bean Conversion:</b> Override the default {@link
BeanConverter} for specialized object introspection</li>
- * <li><b>Enhanced Error Messages:</b> Add context-specific error messages
with parameter substitution</li>
- * <li><b>Fluent Configuration:</b> Chain configuration calls for readable
test setup</li>
- * <li><b>Assertion Context:</b> Provide additional context for complex
assertion scenarios</li>
- * </ul>
- *
- * <h5 class='section'>Basic Usage:</h5>
- * <p class='bjava'>
- * <jc>// Simple usage with default settings</jc>
- * <jsm>assertBean</jsm>(<jsm>args</jsm>(), <jv>myBean</jv>,
<js>"name,age"</js>, <js>"John,30"</js>);
- *
- * <jc>// Custom error message</jc>
- * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"User validation
failed"</js>),
- * <jv>user</jv>, <js>"email,active"</js>,
<js>"[email protected],true"</js>);
- * </p>
- *
- * <h5 class='section'>Custom Bean Converter:</h5>
- * <p class='bjava'>
- * <jc>// Use custom converter for specialized object handling</jc>
- * <jk>var</jk> <jv>customConverter</jv> =
BasicBeanConverter.<jsm>builder</jsm>()
- * .defaultSettings()
- * .addStringifier(MyClass.<jk>class</jk>, <jp>obj</jp> ->
<jp>obj</jp>.getDisplayName())
- * .build();
- *
- *
<jsm>assertBean</jsm>(<jsm>args</jsm>().setBeanConverter(<jv>customConverter</jv>),
- * <jv>myCustomObject</jv>, <js>"property"</js>,
<js>"expectedValue"</js>);
- * </p>
- *
- * <h5 class='section'>Advanced Error Messages:</h5>
- * <p class='bjava'>
- * <jc>// Parameterized error messages</jc>
- * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"Validation
failed for user {0}"</js>, <jv>userId</jv>),
- * <jv>user</jv>, <js>"status"</js>, <js>"ACTIVE"</js>);
- *
- * <jc>// Dynamic error message with supplier</jc>
- * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(() -> <js>"Test
failed at "</js> + Instant.<jsm>now</jsm>()),
- * <jv>result</jv>, <js>"success"</js>, <js>"true"</js>);
- * </p>
- *
- * <h5 class='section'>Fluent Configuration:</h5>
- * <p class='bjava'>
- * <jc>// Chain multiple configuration options</jc>
- * <jk>var</jk> <jv>testArgs</jv> = args()
- * .setBeanConverter(<jv>customConverter</jv>)
- * .setMessage(<js>"Integration test failed for module {0}"</js>,
<jv>moduleName</jv>);
- *
- * <jsm>assertBean</jsm>(<jv>testArgs</jv>, <jv>moduleConfig</jv>,
<js>"enabled,version"</js>, <js>"true,2.1.0"</js>);
- * <jsm>assertBeans</jsm>(<jv>testArgs</jv>, <jv>moduleList</jv>,
<js>"name,status"</js>,
- * <js>"ModuleA,ACTIVE"</js>, <js>"ModuleB,ACTIVE"</js>);
- * </p>
- *
- * <h5 class='section'>Error Message Composition:</h5>
- * <p>When assertion failures occur, error messages are intelligently
composed:</p>
- * <ul>
- * <li><b>Base Message:</b> Custom message set via {@link
#setMessage(String, Object...)} or {@link #setMessage(Supplier)}</li>
- * <li><b>Assertion Context:</b> Specific context provided by individual
assertion methods</li>
- * <li><b>Composite Format:</b> <js>"{base message}, Caused by: {assertion
context}"</js></li>
- * </ul>
- *
- * <p class='bjava'>
- * <jc>// Example error message composition:</jc>
- * <jc>// Base: "User validation failed for user 123"</jc>
- * <jc>// Context: "Bean assertion failed."</jc>
- * <jc>// Result: "User validation failed for user 123, Caused by: Bean
assertion failed."</jc>
- * </p>
- *
- * <h5 class='section'>Thread Safety:</h5>
- * <p>This class is <b>not thread-safe</b> and is intended for single-threaded
test execution.
- * Each test method should create its own instance using {@link
BctAssertions#args()} or create
- * a new instance directly with {@code new AssertionArgs()}.</p>
- *
- * <h5 class='section'>Immutability Considerations:</h5>
- * <p>While this class uses fluent setters that return {@code this} for
chaining, the instance
- * is mutable. For reusable configurations across multiple tests, consider
creating a factory
- * method that returns pre-configured instances.</p>
- *
- * @see BctAssertions#args()
- * @see BeanConverter
- * @see BasicBeanConverter
- */
-public class AssertionArgs {
-
- private BeanConverter beanConverter;
- private Supplier<String> messageSupplier;
-
- /**
- * Creates a new instance with default settings.
- *
- * <p>Instances start with no custom bean converter and no custom error
message.
- * All assertion methods will use default behavior until configured
otherwise.</p>
- */
- public AssertionArgs() { /* no-op */ }
-
- /**
- * Sets a custom {@link BeanConverter} for object introspection and
property access.
- *
- * <p>The custom converter allows fine-tuned control over how objects
are converted to strings,
- * how collections are listified, and how nested properties are
accessed. This is particularly
- * useful for:</p>
- * <ul>
- * <li><b>Custom Object Types:</b> Objects that don't follow
standard JavaBean patterns</li>
- * <li><b>Specialized Formatting:</b> Custom string representations
for assertion comparisons</li>
- * <li><b>Performance Optimization:</b> Cached or optimized
property access strategies</li>
- * <li><b>Domain-Specific Logic:</b> Business-specific property
resolution rules</li>
- * </ul>
- *
- * <h5 class='section'>Example:</h5>
- * <p class='bjava'>
- * <jc>// Create converter with custom stringifiers</jc>
- * <jk>var</jk> <jv>converter</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(Money.<jk>class</jk>, <jp>money</jp> ->
<jp>money</jp>.getAmount().toPlainString())
- * .build();
- *
- * <jc>// Use in assertions</jc>
- *
<jsm>assertBean</jsm>(<jsm>args</jsm>().setBeanConverter(<jv>converter</jv>),
- * <jv>order</jv>, <js>"date,total"</js>,
<js>"2023-12-01,99.99"</js>);
- * </p>
- *
- * @param value The custom bean converter to use. If null, assertions
will fall back to the default converter.
- * @return This instance for method chaining.
- */
- public AssertionArgs setBeanConverter(BeanConverter value) {
- beanConverter = value;
- return this;
- }
-
- /**
- * Sets a parameterized error message for assertion failures.
- *
- * <p>This method uses {@link MessageFormat} to substitute parameters
into the message template.
- * The formatting occurs immediately when this method is called, not
when the assertion fails.</p>
- *
- * <h5 class='section'>Parameter Substitution:</h5>
- * <p>Uses standard MessageFormat patterns:</p>
- * <ul>
- * <li><code>{0}</code> - First parameter</li>
- * <li><code>{1}</code> - Second parameter</li>
- * <li><code>{0,number,#}</code> - Formatted number</li>
- * <li><code>{0,date,short}</code> - Formatted date</li>
- * </ul>
- *
- * <h5 class='section'>Examples:</h5>
- * <p class='bjava'>
- * <jc>// Simple parameter substitution</jc>
- * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"User {0}
validation failed"</js>, <jv>userId</jv>),
- * <jv>user</jv>, <js>"active"</js>, <js>"true"</js>);
- *
- * <jc>// Multiple parameters</jc>
- * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"Test {0}
failed on iteration {1}"</js>, <jv>testName</jv>, <jv>iteration</jv>),
- * <jv>result</jv>, <js>"success"</js>, <js>"true"</js>);
- *
- * <jc>// Number formatting</jc>
- * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"Expected
{0,number,#.##} but got different value"</js>, <jv>expectedValue</jv>),
- * <jv>actual</jv>, <js>"value"</js>, <js>"123.45"</js>);
- * </p>
- *
- * @param message The message template with MessageFormat placeholders.
- * @param args The parameters to substitute into the message template.
- * @return This instance for method chaining.
- */
- public AssertionArgs setMessage(String message, Object...args) {
- messageSupplier = fs(message, args);
- return this;
- }
-
- /**
- * Sets a custom error message supplier for assertion failures.
- *
- * <p>The supplier allows for dynamic message generation, including
context that may only
- * be available at the time of assertion failure. This is useful
for:</p>
- * <ul>
- * <li><b>Timestamps:</b> Including the exact time of failure</li>
- * <li><b>Test State:</b> Including runtime state information</li>
- * <li><b>Expensive Operations:</b> Deferring costly string
operations until needed</li>
- * <li><b>Conditional Messages:</b> Different messages based on
runtime conditions</li>
- * </ul>
- *
- * <h5 class='section'>Example:</h5>
- * <p class='bjava'>
- * <jc>// Dynamic message with timestamp</jc>
- * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(() ->
<js>"Test failed at "</js> + Instant.<jsm>now</jsm>()),
- * <jv>result</jv>, <js>"status"</js>, <js>"SUCCESS"</js>);
- *
- * <jc>// Message with expensive computation</jc>
- * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(() ->
<js>"Failed after "</js> + computeTestDuration() + <js>" ms"</js>),
- * <jv>response</jv>, <js>"error"</js>, <js>"null"</js>);
- * </p>
- *
- * @param value The message supplier. Called only when an assertion
fails.
- * @return This instance for method chaining.
- */
- public AssertionArgs setMessage(Supplier<String> value) {
- messageSupplier = value;
- return this;
- }
-
- /**
- * Gets the configured bean converter, if any.
- *
- * @return An Optional containing the custom converter, or empty if
using default behavior.
- */
- protected Optional<BeanConverter> getBeanConverter() { return
opt(beanConverter); }
-
- /**
- * Gets the base message supplier for composition with
assertion-specific messages.
- *
- * @return The configured message supplier, or null if no custom
message was set.
- */
- protected Supplier<String> getMessage() { return messageSupplier; }
-
- /**
- * Composes the final error message by combining custom and
assertion-specific messages.
- *
- * <p>This method implements the message composition strategy used
throughout the assertion framework:</p>
- * <ul>
- * <li><b>No Custom Message:</b> Returns the assertion-specific
message as-is</li>
- * <li><b>With Custom Message:</b> Returns <code>"{custom}, Caused
by: {assertion}"</code></li>
- * </ul>
- *
- * <p>This allows tests to provide high-level context while preserving
the specific
- * technical details about what assertion failed.</p>
- *
- * @param msg The assertion-specific message template.
- * @param args Parameters for the assertion-specific message.
- * @return A supplier that produces the composed error message.
- */
- protected Supplier<String> getMessage(String msg, Object...args) {
- return messageSupplier == null ? fs(msg, args) : fs("{0},
Caused by: {1}", messageSupplier.get(), f(msg, args));
- }
-}
\ No newline at end of file
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 bb3d14b7e7..24d458f74f 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,6 +27,7 @@ import java.util.*;
import java.util.function.*;
import java.util.stream.*;
+import org.apache.juneau.common.function.ResettableSupplier;
import org.apache.juneau.common.utils.*;
import org.opentest4j.*;
@@ -116,79 +117,171 @@ import org.opentest4j.*;
* <jsm>assertMapped</jsm>(<jv>myObject</jv>, (<jp>obj</jp>, <jp>prop</jp>)
-> <jp>obj</jp>.getProperty(<jp>prop</jp>),
* <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>();
+ * }
+ * }
+ * </p>
*
* <h5 class='section'>Performance and Thread Safety:</h5>
* <p>The BCT framework is designed for high performance with:</p>
* <ul>
* <li><b>Caching:</b> Type-to-handler mappings cached for fast lookup</li>
* <li><b>Thread Safety:</b> All operations are thread-safe for concurrent
testing</li>
+ * <li><b>Thread-Local Storage:</b> Default converter is stored per-thread,
allowing parallel test execution</li>
* <li><b>Minimal Allocation:</b> Efficient object reuse and minimal
temporary objects</li>
* </ul>
*
* @see BeanConverter
* @see BasicBeanConverter
+ * @see #setConverter(BeanConverter)
+ * @see #resetConverter()
*/
public class BctAssertions {
- private static final BeanConverter DEFAULT_CONVERTER =
BasicBeanConverter.DEFAULT;
+ // 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<>();
/**
- * Creates a new {@link AssertionArgs} instance for configuring
assertion behavior.
+ * Gets the bean converter for the current thread.
*
- * <p>AssertionArgs provides fluent configuration for customizing
assertion behavior, including:</p>
- * <ul>
- * <li><b>Custom Messages:</b> Static strings, parameterized with
<code>MessageFormat</code>, or dynamic suppliers</li>
- * <li><b>Custom Bean Converters:</b> Override default
object-to-string conversion behavior</li>
- * <li><b>Timeout Configuration:</b> Set timeouts for operations
that may take time</li>
- * </ul>
+ * <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>
*
- * <h5 class='section'>Usage Examples:</h5>
- * <p class='bjava'>
- * <jc>// Static message</jc>
- * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"User
validation failed"</js>),
- * <jv>user</jv>, <js>"name,age"</js>, <js>"John,30"</js>);
+ * @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();
+ }
+
+ /**
+ * Sets a custom bean converter for the current thread.
*
- * <jc>// Parameterized message</jc>
- * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"Test
failed for user {0}"</js>, <jv>userId</jv>),
- * <jv>user</jv>, <js>"status"</js>, <js>"ACTIVE"</js>);
+ * <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>
*
- * <jc>// Dynamic message with supplier</jc>
- * <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(() ->
<js>"Test failed at "</js> + Instant.<jsm>now</jsm>()),
- * <jv>result</jv>, <js>"success"</js>, <js>"true"</js>);
+ * <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>
*
- * <jc>// Custom bean converter</jc>
- * <jk>var</jk> <jv>converter</jv> =
BasicBeanConverter.<jsm>builder</jsm>()
+ * <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();
- *
<jsm>assertBean</jsm>(<jsm>args</jsm>().setBeanConverter(<jv>converter</jv>),
- * <jv>event</jv>, <js>"date"</js>, <js>"2023-12-01"</js>);
+ * 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>
*
- * @return A new AssertionArgs instance for fluent configuration
- * @see AssertionArgs
+ * <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 AssertionArgs args() {
- return new AssertionArgs();
+ public static void setConverter(BeanConverter converter) {
+ assertArgNotNull("converter", converter);
+ CONVERTER_OVERRIDE.set(converter);
}
/**
- * Same as {@link #assertBean(Object, String, String)} but with
configurable assertion behavior.
+ * Resets the bean converter for the current thread to the system
default.
*
- * @param args Assertion configuration. See {@link #args()} for usage
examples.
- * @param actual The bean to test. Must not be null.
- * @param fields A comma-delimited list of bean property names
(supports nested syntax).
- * @param expected The expected property values as a comma-delimited
string.
- * @see #assertBean(Object, String, String)
- * @see #args()
+ * <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 assertBean(AssertionArgs args, Object actual, String
fields, String expected) {
- assertNotNull(actual, "Actual was null.");
- assertArgNotNull("args", args);
- assertArgNotNull("fields", fields);
- assertArgNotNull("expected", expected);
- assertEquals(expected, tokenize(fields).stream().map(x ->
args.getBeanConverter().orElse(DEFAULT_CONVERTER).getNested(actual,
x)).collect(joining(",")),
- args.getMessage("Bean assertion failed."));
+ public static void resetConverter() {
+ CONVERTER_OVERRIDE.remove();
+ CONVERTER_SUPPLIER.get().reset();
+ }
+
+ /**
+ * Composes an error message from an optional custom message and a
default message.
+ *
+ * <p>If a custom message is provided, it is composed with the default
message in the format:
+ * <js>"{custom}, Caused by: {default}"</js>. Otherwise, the default
message is returned.</p>
+ *
+ * @param customMessage Optional custom message supplier. Can be
<jk>null</jk>.
+ * @param defaultMessage Default message template.
+ * @param defaultArgs Arguments for the default message template.
+ * @return A supplier that produces the composed error message.
+ */
+ private static Supplier<String> composeMessage(Supplier<String>
customMessage, String defaultMessage, Object...defaultArgs) {
+ if (customMessage == null) {
+ return fs(defaultMessage, defaultArgs);
+ }
+ return fs("{0}, Caused by: {1}", customMessage.get(),
f(defaultMessage, defaultArgs));
}
/**
@@ -198,7 +291,7 @@ public class BctAssertions {
* patterns including nested objects, collections, arrays, method
chaining, direct field access, collection iteration
* with <js>"#{property}"</js> syntax, and universal
<js>"length"</js>/<js>"size"</js> properties for all collection types.</p>
*
- * <p>The method uses the {@link BasicBeanConverter#DEFAULT} converter
internally for object introspection
+ * <p>The method uses the default converter (set via {@link
#setConverter(BeanConverter)}) for object introspection
* and value extraction. The converter provides sophisticated property
access through the {@link BeanConverter}
* interface, supporting multiple fallback mechanisms for accessing
object properties and values.</p>
*
@@ -209,6 +302,12 @@ public class BctAssertions {
*
* <jc>// Test single property</jc>
* <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"name"</js>,
<js>"John"</js>);
+ *
+ * <jc>// With custom error message</jc>
+ * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"name,age"</js>,
<js>"John,30"</js>, () -> <js>"User validation failed"</js>);
+ *
+ * <jc>// With formatted message using Utils.fs() for convenient
message suppliers with arguments</jc>
+ * <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"name,age"</js>,
<js>"John,30"</js>, <jsm>fs</jsm>(<js>"User {0} validation failed"</js>,
<js>"John"</js>));
* </p>
*
* <h5 class='section'>Nested Property Testing:</h5>
@@ -354,6 +453,9 @@ public class BctAssertions {
* <li><b>Public fields</b> (direct field access)</li>
* </ol>
*
+ * @param message Optional custom error message supplier. If provided,
will be composed with the default assertion message.
+ * Use {@link
org.apache.juneau.common.utils.Utils#fs(String, Object...) Utils.fs()} to
conveniently
+ * create message suppliers with format arguments (e.g.,
<code>fs("User {0} validation failed", userName)</code>).
* @param actual The bean object to test. Must not be null.
* @param fields Comma-delimited list of property names to test.
Supports nested syntax with {}.
* @param expected Comma-delimited list of expected values. Must match
the order of fields.
@@ -361,58 +463,35 @@ public class BctAssertions {
* @throws AssertionError if any property values don't match expected
values
* @see BeanConverter
* @see BasicBeanConverter
+ * @see #setConverter(BeanConverter)
+ * @see org.apache.juneau.common.utils.Utils#fs(String, Object...)
*/
- public static void assertBean(Object actual, String fields, String
expected) {
- assertBean(args(), actual, fields, expected);
+ public static void assertBean(Supplier<String> message, Object actual,
String fields, String expected) {
+ assertNotNull(actual, "Actual was null.");
+ assertArgNotNull("fields", fields);
+ assertArgNotNull("expected", expected);
+ var converter = getConverter();
+ assertEquals(expected, tokenize(fields).stream().map(x ->
converter.getNested(actual, x)).collect(joining(",")),
+ composeMessage(message, "Bean assertion failed."));
}
/**
- * Same as {@link #assertBeans(Object, String, String...)} but with
configurable assertion behavior.
+ * Asserts that the fields/properties on the specified bean are the
specified values after being converted to strings.
*
- * @param args Assertion configuration. See {@link #args()} for usage
examples.
- * @param actual The collection of beans to test. Must not be null.
- * @param fields A comma-delimited list of bean property names
(supports nested syntax).
- * @param expected Array of expected value strings, one per bean.
- * @see #assertBeans(Object, String, String...)
- * @see #args()
+ * <p>Same as {@link #assertBean(Supplier, Object, String, String)} but
without a custom message.</p>
+ *
+ * @param actual The bean object to test. Must not be null.
+ * @param fields Comma-delimited list of property names to test.
Supports nested syntax with {}.
+ * @param expected Comma-delimited list of expected values. Must match
the order of fields.
+ * @throws NullPointerException if the bean is null
+ * @throws AssertionError if any property values don't match expected
values
+ * @see #assertBean(Supplier, Object, String, String)
*/
- public static void assertBeans(AssertionArgs args, Object actual,
String fields, String...expected) {
- assertNotNull(actual, "Value was null.");
- assertArgNotNull("args", args);
- assertArgNotNull("fields", fields);
- assertArgNotNull("expected", expected);
-
- var converter =
args.getBeanConverter().orElse(DEFAULT_CONVERTER);
- var tokens = tokenize(fields);
- var errors = new ArrayList<AssertionFailedError>();
- var actualList = converter.listify(actual);
-
- if (ne(expected.length, actualList.size())) {
- errors.add(assertEqualsFailed(expected.length,
actualList.size(), args.getMessage("Wrong number of beans.")));
- } else {
- for (var i = 0; i < actualList.size(); i++) {
- var i2 = i;
- var e = converter.stringify(expected[i]);
- var a = tokens.stream().map(x ->
converter.getNested(actualList.get(i2), x)).collect(joining(","));
- if (ne(e, a)) {
- errors.add(assertEqualsFailed(e, a,
args.getMessage("Bean at row <{0}> did not match.", i)));
- }
- }
- }
-
- if (errors.isEmpty())
- return;
-
- var actualStrings = new ArrayList<String>();
- for (var o : actualList) {
- actualStrings.add(tokens.stream().map(x ->
converter.getNested(o, x)).collect(joining(",")));
- }
-
- throw
assertEqualsFailed(Stream.of(expected).map(StringUtils::escapeForJava).collect(joining("\",
\"", "\"", "\"")),
-
actualStrings.stream().map(StringUtils::escapeForJava).collect(joining("\",
\"", "\"", "\"")),
- args.getMessage("{0} bean assertions failed:\n{1}",
errors.size(), errors.stream().map(x ->
x.getMessage()).collect(joining("\n"))));
+ public static void assertBean(Object actual, String fields, String
expected) {
+ assertBean(null, actual, fields, expected);
}
+
/**
* Asserts that multiple beans in a collection have the expected
property values.
*
@@ -471,28 +550,58 @@ public class BctAssertions {
* @see #assertBean(Object, String, String)
*/
public static void assertBeans(Object actual, String fields,
String...expected) {
- assertBeans(args(), actual, fields, expected);
+ assertBeans(null, actual, fields, expected);
}
/**
- * Same as {@link #assertContains(String, Object)} but with
configurable assertion behavior.
+ * Asserts that multiple beans in a collection have the expected
property values.
*
- * @param args Assertion configuration. See {@link #args()} for usage
examples.
- * @param expected The substring that must be present.
- * @param actual The object to test. Must not be null.
- * @see #assertContains(String, Object)
- * @see #args()
+ * <p>Same as {@link #assertBeans(Object, String, String...)} but with
a custom error message.</p>
+ *
+ * @param message Optional custom error message supplier. If provided,
will be composed with the default assertion message.
+ * @param actual The collection of beans to check. Must not be null.
+ * @param fields A comma-delimited list of bean property names
(supports nested syntax).
+ * @param expected Array of expected value strings, one per bean. Each
string contains comma-delimited values matching the fields.
+ * @throws AssertionError if the collection size doesn't match values
array length or if any bean properties don't match
+ * @see #assertBean(Object, String, String)
*/
- public static void assertContains(AssertionArgs args, String expected,
Object actual) {
- assertArgNotNull("args", args);
- assertArgNotNull("expected", expected);
- assertArgNotNull("actual", actual);
+ public static void assertBeans(Supplier<String> message, Object actual,
String fields, String...expected) {
assertNotNull(actual, "Value was null.");
+ assertArgNotNull("fields", fields);
+ assertArgNotNull("expected", expected);
+
+ var converter = getConverter();
+ var tokens = tokenize(fields);
+ var errors = new ArrayList<AssertionFailedError>();
+ List<Object> actualList = converter.listify(actual);
+
+ if (ne(expected.length, actualList.size())) {
+ errors.add(assertEqualsFailed(expected.length,
actualList.size(), composeMessage(message, "Wrong number of beans.")));
+ } else {
+ for (var i = 0; i < actualList.size(); i++) {
+ var i2 = i;
+ var e = converter.stringify(expected[i]);
+ var a = tokens.stream().map(x ->
converter.getNested(actualList.get(i2), x)).collect(joining(","));
+ if (ne(e, a)) {
+ errors.add(assertEqualsFailed(e, a,
composeMessage(message, "Bean at row <{0}> did not match.", i)));
+ }
+ }
+ }
+
+ if (errors.isEmpty())
+ return;
+
+ var actualStrings = new ArrayList<String>();
+ for (var o : actualList) {
+ actualStrings.add(tokens.stream().map(x ->
converter.getNested(o, x)).collect(joining(",")));
+ }
- var a =
args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(actual);
- assertTrue(a.contains(expected), args.getMessage("String did
not contain expected substring. ==> expected: <{0}> but was: <{1}>", expected,
a));
+ throw
assertEqualsFailed(Stream.of(expected).map(StringUtils::escapeForJava).collect(joining("\",
\"", "\"", "\"")),
+
actualStrings.stream().map(StringUtils::escapeForJava).collect(joining("\",
\"", "\"", "\"")),
+ composeMessage(message, "{0} bean assertions
failed:\n{1}", errors.size(), errors.stream().map(x ->
x.getMessage()).collect(joining("\n"))));
}
+
/**
* Asserts that the string representation of an object contains the
expected substring.
*
@@ -512,36 +621,72 @@ public class BctAssertions {
* <jsm>assertContains</jsm>(<js>"\"name\":\"John\""</js>,
<jv>jsonResponse</jv>);
* </p>
*
+ * @param message Optional custom error message supplier. If provided,
will be composed with the default assertion message.
* @param expected The substring that must be present in the actual
object's string representation
* @param actual The object to test. Must not be null.
* @throws AssertionError if the actual object is null or its string
representation doesn't contain the expected substring
* @see #assertContainsAll(Object, String...) for multiple substring
assertions
* @see #assertString(String, Object) for exact string matching
*/
+ public static void assertContains(Supplier<String> message, String
expected, Object actual) {
+ assertArgNotNull("expected", expected);
+ assertArgNotNull("actual", actual);
+ assertNotNull(actual, "Value was null.");
+
+ var a = getConverter().stringify(actual);
+ assertTrue(a.contains(expected), composeMessage(message,
"String did not contain expected substring. ==> expected: <{0}> but was:
<{1}>", expected, a));
+ }
+
+ /**
+ * Asserts that the string representation of an object contains the
expected substring.
+ *
+ * <p>Same as {@link #assertContains(Supplier, String, Object)} but
without a custom message.</p>
+ *
+ * @param expected The substring that must be present in the actual
object's string representation
+ * @param actual The object to test. Must not be null.
+ * @throws AssertionError if the actual object is null or its string
representation doesn't contain the expected substring
+ * @see #assertContains(Supplier, String, Object)
+ */
public static void assertContains(String expected, Object actual) {
- assertContains(args(), expected, actual);
+ assertContains(null, expected, actual);
}
+
/**
- * Same as {@link #assertContainsAll(Object, String...)} but with
configurable assertion behavior.
+ * Asserts that the string representation of an object contains all
specified substrings.
+ *
+ * <p>This method is similar to {@link #assertContains(String, Object)}
but tests for multiple
+ * required substrings. All provided substrings must be present in the
actual object's string
+ * representation for the assertion to pass.</p>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test that error contains multiple pieces of
information</jc>
+ * <jsm>assertContainsAll</jsm>(<jv>exception</jv>,
<js>"FileNotFoundException"</js>, <js>"config.xml"</js>, <js>"/etc"</js>);
*
- * @param args Assertion configuration. See {@link #args()} for usage
examples.
+ * <jc>// Test that user object contains expected fields</jc>
+ * <jsm>assertContainsAll</jsm>(<jv>user</jv>, <js>"name=John"</js>,
<js>"age=30"</js>, <js>"status=ACTIVE"</js>);
+ *
+ * <jc>// Test log output contains all required entries</jc>
+ * <jsm>assertContainsAll</jsm>(<jv>logOutput</jv>, <js>"INFO"</js>,
<js>"Started"</js>, <js>"Successfully"</js>);
+ * </p>
+ *
+ * @param message Optional custom error message supplier. If provided,
will be composed with the default assertion message.
* @param actual The object to test. Must not be null.
- * @param expected Multiple substrings that must all be present.
- * @see #assertContainsAll(Object, String...)
- * @see #args()
+ * @param expected Multiple substrings that must all be present in the
actual object's string representation
+ * @throws AssertionError if the actual object is null or its string
representation doesn't contain all expected substrings
+ * @see #assertContains(String, Object) for single substring assertions
*/
- public static void assertContainsAll(AssertionArgs args, Object actual,
String...expected) {
- assertArgNotNull("args", args);
+ public static void assertContainsAll(Supplier<String> message, Object
actual, String...expected) {
assertArgNotNull("expected", expected);
assertNotNull(actual, "Value was null.");
- var a =
args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(actual);
+ var a = getConverter().stringify(actual);
var errors = new ArrayList<AssertionFailedError>();
for (var e : expected) {
if (! a.contains(e)) {
- errors.add(assertEqualsFailed(true, false,
args.getMessage("String did not contain expected substring. ==> expected:
<{0}> but was: <{1}>", e, a)));
+ errors.add(assertEqualsFailed(true, false,
composeMessage(message, "String did not contain expected substring. ==>
expected: <{0}> but was: <{1}>", e, a)));
}
}
@@ -559,51 +704,23 @@ public class BctAssertions {
}
throw
assertEqualsFailed(missingSubstrings.stream().map(StringUtils::escapeForJava).collect(joining("\",
\"", "\"", "\"")), escapeForJava(a),
- args.getMessage("{0} substring assertions
failed:\n{1}", errors.size(), errors.stream().map(x ->
x.getMessage()).collect(joining("\n"))));
+ composeMessage(message, "{0} substring assertions
failed:\n{1}", errors.size(), errors.stream().map(x ->
x.getMessage()).collect(joining("\n"))));
}
/**
* Asserts that the string representation of an object contains all
specified substrings.
*
- * <p>This method is similar to {@link #assertContains(String, Object)}
but tests for multiple
- * required substrings. All provided substrings must be present in the
actual object's string
- * representation for the assertion to pass.</p>
- *
- * <h5 class='section'>Usage Examples:</h5>
- * <p class='bjava'>
- * <jc>// Test that error contains multiple pieces of
information</jc>
- * <jsm>assertContainsAll</jsm>(<jv>exception</jv>,
<js>"FileNotFoundException"</js>, <js>"config.xml"</js>, <js>"/etc"</js>);
- *
- * <jc>// Test that user object contains expected fields</jc>
- * <jsm>assertContainsAll</jsm>(<jv>user</jv>, <js>"name=John"</js>,
<js>"age=30"</js>, <js>"status=ACTIVE"</js>);
- *
- * <jc>// Test log output contains all required entries</jc>
- * <jsm>assertContainsAll</jsm>(<jv>logOutput</jv>, <js>"INFO"</js>,
<js>"Started"</js>, <js>"Successfully"</js>);
- * </p>
+ * <p>Same as {@link #assertContainsAll(Supplier, Object, String...)}
but without a custom message.</p>
*
* @param actual The object to test. Must not be null.
* @param expected Multiple substrings that must all be present in the
actual object's string representation
* @throws AssertionError if the actual object is null or its string
representation doesn't contain all expected substrings
- * @see #assertContains(String, Object) for single substring assertions
+ * @see #assertContainsAll(Supplier, Object, String...)
*/
public static void assertContainsAll(Object actual, String...expected) {
- assertContainsAll(args(), actual, expected);
+ assertContainsAll(null, actual, expected);
}
- /**
- * Same as {@link #assertEmpty(Object)} but with configurable assertion
behavior.
- *
- * @param args Assertion configuration. See {@link #args()} for usage
examples.
- * @param value The object to test. Must not be null.
- * @see #assertEmpty(Object)
- * @see #args()
- */
- public static void assertEmpty(AssertionArgs args, Object value) {
- assertArgNotNull("args", args);
- assertNotNull(value, "Value was null.");
- var size =
args.getBeanConverter().orElse(DEFAULT_CONVERTER).size(value);
- assertEquals(0, size, args.getMessage("Value was not empty.
Size=<{0}>", size));
- }
/**
* Asserts that a collection-like object, Optional, Value, String, or
array is not null and empty.
@@ -645,72 +762,32 @@ public class BctAssertions {
* <jsm>assertEmpty</jsm>(<jk>new</jk> HashMap<>());
* </p>
*
+ * @param message Optional custom error message supplier. If provided,
will be composed with the default assertion message.
* @param value The object to test. Must not be null.
* @throws AssertionError if the object is null or not empty
* @see #assertNotEmpty(Object) for testing non-empty collections
* @see #assertSize(int, Object) for testing specific sizes
*/
- public static void assertEmpty(Object value) {
- assertEmpty(args(), value);
+ public static void assertEmpty(Supplier<String> message, Object value) {
+ assertNotNull(value, "Value was null.");
+ var size = getConverter().size(value);
+ assertEquals(0, size, composeMessage(message, "Value was not
empty. Size=<{0}>", size));
}
/**
- * Same as {@link #assertList(Object, Object...)} but with configurable
assertion behavior.
+ * Asserts that a collection-like object, Optional, Value, String, or
array is not null and empty.
*
- * @param args Assertion configuration. See {@link #args()} for usage
examples.
- * @param actual The List to test. Must not be null.
- * @param expected Multiple arguments of expected values.
- * @see #assertList(Object, Object...)
- * @see #args()
+ * <p>Same as {@link #assertEmpty(Supplier, Object)} but without a
custom message.</p>
+ *
+ * @param value The object to test. Must not be null.
+ * @throws AssertionError if the object is null or not empty
+ * @see #assertEmpty(Supplier, Object)
*/
- @SuppressWarnings("unchecked")
- public static void assertList(AssertionArgs args, Object actual,
Object...expected) {
- assertArgNotNull("args", args);
- assertArgNotNull("expected", expected);
- assertNotNull(actual, "Value was null.");
-
- var converter =
args.getBeanConverter().orElse(DEFAULT_CONVERTER);
- var list = converter.listify(actual);
- var errors = new ArrayList<AssertionFailedError>();
-
- if (ne(expected.length, list.size())) {
- errors.add(assertEqualsFailed(expected.length,
list.size(), args.getMessage("Wrong list length.")));
- } else {
- for (var i = 0; i < expected.length; i++) {
- var x = list.get(i);
- var e = expected[i];
- if (e instanceof String e2) {
- if (ne(e2, converter.stringify(x))) {
-
errors.add(assertEqualsFailed(e2, converter.stringify(x),
args.getMessage("Element at index {0} did not match.", i)));
- }
- } else if (e instanceof Predicate e2) { //
NOSONAR
- if (! e2.test(x)) {
- errors.add(new
AssertionFailedError(args.getMessage("Element at index {0} did not pass
predicate. ==> actual: <{1}>", i, converter.stringify(x)).get()));
- }
- } else {
- if (ne(e, x)) {
-
errors.add(assertEqualsFailed(e, x, args.getMessage("Element at index {0} did
not match. ==> expected: <{1}({2})> but was: <{3}({4})>", i, e, cns(e), x,
cns(x))));
- }
- }
- }
- }
-
- if (errors.isEmpty())
- return;
-
- var actualStrings = new ArrayList<String>();
- for (var o : list) {
- actualStrings.add(converter.stringify(o));
- }
-
- if (errors.size() == 1)
- throw errors.get(0);
-
- throw
assertEqualsFailed(Stream.of(expected).map(converter::stringify).map(StringUtils::escapeForJava).collect(joining("\",
\"", "[\"", "\"]")),
-
actualStrings.stream().map(StringUtils::escapeForJava).collect(joining("\",
\"", "[\"", "\"]")),
- args.getMessage("{0} list assertions failed:\n{1}",
errors.size(), errors.stream().map(x ->
x.getMessage()).collect(joining("\n"))));
+ public static void assertEmpty(Object value) {
+ assertEmpty(null, value);
}
+
/**
* Asserts that a List or List-like object contains the expected values
using flexible comparison logic.
*
@@ -757,28 +834,77 @@ public class BctAssertions {
* <jsm>assertList</jsm>(List.<jsm>of</jsm>(<jv>myBean1</jv>,
<jv>myBean2</jv>), <jv>myBean1</jv>, <jv>myBean2</jv>); <jc>// Custom
objects</jc>
* </p>
*
+ * @param message Optional custom error message supplier. If provided,
will be composed with the default assertion message.
* @param actual The List to test. Must not be null.
* @param expected Multiple arguments of expected values.
* Can be Strings (readable format comparison),
Predicates (functional testing), or Objects (direct equality).
* @throws AssertionError if the List size or contents don't match
expected values
*/
- public static void assertList(Object actual, Object...expected) {
- assertList(args(), actual, expected);
+ @SuppressWarnings("unchecked")
+ public static void assertList(Supplier<String> message, Object actual,
Object...expected) {
+ assertArgNotNull("expected", expected);
+ assertArgNotNull("actual", actual);
+
+ var converter = getConverter();
+ List<Object> list = converter.listify(actual);
+ var errors = new ArrayList<AssertionFailedError>();
+
+ if (ne(expected.length, list.size())) {
+ errors.add(assertEqualsFailed(expected.length,
list.size(), composeMessage(message, "Wrong list length.")));
+ } else {
+ for (var i = 0; i < expected.length; i++) {
+ var x = list.get(i);
+ var e = expected[i];
+ if (e instanceof String e2) {
+ if (ne(e2, converter.stringify(x))) {
+
errors.add(assertEqualsFailed(e2, converter.stringify(x),
composeMessage(message, "Element at index {0} did not match.", i)));
+ }
+ } else if (e instanceof Predicate e2) { //
NOSONAR
+ if (! e2.test(x)) {
+ errors.add(new
AssertionFailedError(composeMessage(message, "Element at index {0} did not pass
predicate. ==> actual: <{1}>", i, converter.stringify(x)).get()));
+ }
+ } else {
+ if (ne(e, x)) {
+
errors.add(assertEqualsFailed(e, x, composeMessage(message, "Element at index
{0} did not match. ==> expected: <{1}({2})> but was: <{3}({4})>", i, e,
cns(e), x, cns(x))));
+ }
+ }
+ }
+ }
+
+ if (errors.isEmpty())
+ return;
+
+ var actualStrings = new ArrayList<String>();
+ for (var o : list) {
+ actualStrings.add(converter.stringify(o));
+ }
+
+ if (errors.size() == 1)
+ throw errors.get(0);
+
+ throw
assertEqualsFailed(Stream.of(expected).map(converter::stringify).map(StringUtils::escapeForJava).collect(joining("\",
\"", "[\"", "\"]")),
+
actualStrings.stream().map(StringUtils::escapeForJava).collect(joining("\",
\"", "[\"", "\"]")),
+ composeMessage(message, "{0} list assertions
failed:\n{1}", errors.size(), errors.stream().map(x ->
x.getMessage()).collect(joining("\n"))));
}
/**
- * Same as {@link #assertMap(Map, Object...)} but with configurable
assertion behavior.
+ * Asserts that a List or List-like object contains the expected values
using flexible comparison logic.
*
- * @param args Assertion configuration. See {@link #args()} for usage
examples.
- * @param actual The Map to test. Must not be null.
- * @param expected Multiple arguments of expected map entries.
- * @see #assertMap(Map, Object...)
- * @see #args()
+ * <p>Same as {@link #assertList(Supplier, Object, Object...)} but
without a custom message.</p>
+ *
+ * @param actual The List to test. Must not be null.
+ * @param expected Multiple arguments of expected values.
+ * Can be Strings (readable format comparison),
Predicates (functional testing), or Objects (direct equality).
+ * @throws IllegalArgumentException if actual is null
+ * @throws AssertionError if the List size or contents don't match
expected values
+ * @see #assertList(Supplier, Object, Object...)
*/
- public static void assertMap(AssertionArgs args, Map<?,?> actual,
Object...expected) {
- assertList(args, actual, expected);
+ public static void assertList(Object actual, Object...expected) {
+ assertArgNotNull("actual", actual);
+ assertList(null, actual, expected);
}
+
/**
* Asserts that a Map contains the expected key/value pairs using
flexible comparison logic.
*
@@ -833,44 +959,33 @@ public class BctAssertions {
* </ul>
* <p>This ensures predictable test results regardless of the original
map implementation.</p>
*
+ * @param message Optional custom error message supplier. If provided,
will be composed with the default assertion message.
* @param actual The Map to test. Must not be null.
* @param expected Multiple arguments of expected map entries.
* Can be Strings (readable format comparison),
Predicates (functional testing), or Objects (direct equality).
* @throws AssertionError if the Map size or contents don't match
expected values
- * @see #assertList(Object, Object...)
+ * @see #assertList(Supplier, Object, Object...)
*/
- public static void assertMap(Map<?,?> actual, Object...expected) {
- assertList(args(), actual, expected);
+ public static void assertMap(Supplier<String> message, Map<?,?> actual,
Object...expected) {
+ assertList(message, actual, expected);
}
/**
- * Same as {@link #assertMapped(Object, BiFunction, String, String)}
but with configurable assertion behavior.
+ * Asserts that a Map contains the expected key/value pairs using
flexible comparison logic.
*
- * @param <T> The object type being tested.
- * @param args Assertion configuration. See {@link #args()} for usage
examples.
- * @param actual The object to test. Must not be null.
- * @param function Custom property access function.
- * @param properties A comma-delimited list of property names.
- * @param expected The expected property values as a comma-delimited
string.
- * @see #assertMapped(Object, BiFunction, String, String)
- * @see #args()
+ * <p>Same as {@link #assertMap(Supplier, Map, Object...)} but without
a custom message.</p>
+ *
+ * @param actual The Map to test. Must not be null.
+ * @param expected Multiple arguments of expected map entries.
+ * Can be Strings (readable format comparison),
Predicates (functional testing), or Objects (direct equality).
+ * @throws AssertionError if the Map size or contents don't match
expected values
+ * @see #assertMap(Supplier, Map, Object...)
*/
- public static <T> void assertMapped(AssertionArgs args, T actual,
BiFunction<T,String,Object> function, String properties, String expected) {
- assertNotNull(actual, "Value was null.");
- assertArgNotNull("args", args);
- assertArgNotNull("function", function);
- assertArgNotNull("properties", properties);
- assertArgNotNull("expected", expected);
-
- var m = new LinkedHashMap<String,Object>();
- for (var p : tokenize(properties)) {
- var pv = p.getValue();
- m.put(pv, safe(() -> function.apply(actual, pv)));
- }
-
- assertBean(args, m, properties, expected);
+ public static void assertMap(Map<?,?> actual, Object...expected) {
+ assertMap(null, actual, expected);
}
+
/**
* Asserts that mapped property access on an object returns expected
values using a custom BiFunction.
*
@@ -884,38 +999,49 @@ public class BctAssertions {
* for value stringification and nested property access.</p>
*
* @param <T> The type of object being tested
+ * @param message Optional custom error message supplier. If provided,
will be composed with the default assertion message.
* @param actual The object to test properties on
* @param function The BiFunction that extracts property values.
Receives (<jp>object</jp>, <jp>propertyName</jp>) and returns the property
value.
* @param properties Comma-delimited list of property names to test
* @param expected Comma-delimited list of expected values (exceptions
become simple class names)
* @throws AssertionError if any mapped property values don't match
expected values
- * @see #assertBean(Object, String, String)
+ * @see #assertBean(Supplier, Object, String, String)
* @see BeanConverter
* @see BasicBeanConverter
*/
- public static <T> void assertMapped(T actual,
BiFunction<T,String,Object> function, String properties, String expected) {
- assertMapped(args(), actual, function, properties, expected);
+ public static <T> void assertMapped(Supplier<String> message, T actual,
BiFunction<T,String,Object> function, String properties, String expected) {
+ assertNotNull(actual, "Value was null.");
+ assertArgNotNull("function", function);
+ assertArgNotNull("properties", properties);
+ assertArgNotNull("expected", expected);
+
+ var m = new LinkedHashMap<String,Object>();
+ for (var p : tokenize(properties)) {
+ var pv = p.getValue();
+ m.put(pv, safe(() -> function.apply(actual, pv)));
+ }
+
+ assertBean(message, m, properties, expected);
}
/**
- * Same as {@link #assertMatchesGlob(String, Object)} but with
configurable assertion behavior.
+ * Asserts that mapped property access on an object returns expected
values using a custom BiFunction.
*
- * @param args Assertion configuration. See {@link #args()} for usage
examples.
- * @param pattern The glob-style pattern to match against.
- * @param value The object to test. Must not be null.
- * @see #assertMatchesGlob(String, Object)
- * @see #args()
+ * <p>Same as {@link #assertMapped(Supplier, Object, BiFunction,
String, String)} but without a custom message.</p>
+ *
+ * @param <T> The type of object being tested
+ * @param actual The object to test properties on
+ * @param function The BiFunction that extracts property values.
Receives (<jp>object</jp>, <jp>propertyName</jp>) and returns the property
value.
+ * @param properties Comma-delimited list of property names to test
+ * @param expected Comma-delimited list of expected values (exceptions
become simple class names)
+ * @throws AssertionError if any mapped property values don't match
expected values
+ * @see #assertMapped(Supplier, Object, BiFunction, String, String)
*/
- public static void assertMatchesGlob(AssertionArgs args, String
pattern, Object value) {
- assertArgNotNull("args", args);
- assertArgNotNull("pattern", pattern);
- assertNotNull(value, "Value was null.");
-
- var v =
args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(value);
- var m = StringUtils.getGlobMatchPattern(pattern).matcher(v);
- assertTrue(m.matches(), args.getMessage("Pattern didn''t match.
==> pattern: <{0}> but was: <{1}>", pattern, v));
+ public static <T> void assertMapped(T actual,
BiFunction<T,String,Object> function, String properties, String expected) {
+ assertMapped(null, actual, function, properties, expected);
}
+
/**
* Asserts that an object's string representation matches the specified
glob-style pattern.
*
@@ -943,31 +1069,37 @@ public class BctAssertions {
* <jsm>assertMatchesGlob</jsm>(<js>"log_*_?.txt"</js>,
<jv>logFile</jv>);
* </p>
*
+ * @param message Optional custom error message supplier. If provided,
will be composed with the default assertion message.
* @param pattern The glob-style pattern to match against.
* @param value The object to test. Must not be null.
* @throws AssertionError if the value is null or its string
representation doesn't match the pattern
- * @see #assertString(String, Object) for exact string matching
- * @see #assertContains(String, Object) for substring matching
+ * @see #assertString(Supplier, String, Object) for exact string
matching
+ * @see #assertContains(Supplier, String, Object) for substring matching
*/
- public static void assertMatchesGlob(String pattern, Object value) {
- assertMatchesGlob(args(), pattern, value);
+ public static void assertMatchesGlob(Supplier<String> message, String
pattern, Object value) {
+ assertArgNotNull("pattern", pattern);
+ assertNotNull(value, "Value was null.");
+
+ var v = 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));
}
/**
- * Same as {@link #assertNotEmpty(Object)} but with configurable
assertion behavior.
+ * Asserts that an object's string representation matches the specified
glob-style pattern.
*
- * @param args Assertion configuration. See {@link #args()} for usage
examples.
+ * <p>Same as {@link #assertMatchesGlob(Supplier, String, Object)} but
without a custom message.</p>
+ *
+ * @param pattern The glob-style pattern to match against.
* @param value The object to test. Must not be null.
- * @see #assertNotEmpty(Object)
- * @see #args()
+ * @throws AssertionError if the value is null or its string
representation doesn't match the pattern
+ * @see #assertMatchesGlob(Supplier, String, Object)
*/
- public static void assertNotEmpty(AssertionArgs args, Object value) {
- assertArgNotNull("args", args);
- assertNotNull(value, "Value was null.");
- var size =
args.getBeanConverter().orElse(DEFAULT_CONVERTER).size(value);
- assertTrue(size > 0, args.getMessage("Value was empty."));
+ public static void assertMatchesGlob(String pattern, Object value) {
+ assertMatchesGlob(null, pattern, value);
}
+
/**
* Asserts that a collection-like object, Optional, Value, String, or
array is not null and not empty.
*
@@ -1008,31 +1140,32 @@ public class BctAssertions {
* <jsm>assertNotEmpty</jsm>(Map.<jsm>of</jsm>(<js>"key"</js>,
<js>"value"</js>));
* </p>
*
+ * @param message Optional custom error message supplier. If provided,
will be composed with the default assertion message.
* @param value The object to test. Must not be null.
* @throws AssertionError if the object is null or empty
- * @see #assertEmpty(Object) for testing empty collections
- * @see #assertSize(int, Object) for testing specific sizes
+ * @see #assertEmpty(Supplier, Object) for testing empty collections
+ * @see #assertSize(Supplier, int, Object) for testing specific sizes
*/
- public static void assertNotEmpty(Object value) {
- assertNotEmpty(args(), value);
+ public static void assertNotEmpty(Supplier<String> message, Object
value) {
+ assertNotNull(value, "Value was null.");
+ int size = getConverter().size(value);
+ assertTrue(size > 0, composeMessage(message, "Value was
empty."));
}
/**
- * Same as {@link #assertSize(int, Object)} but with configurable
assertion behavior.
+ * Asserts that a collection-like object, Optional, Value, String, or
array is not null and not empty.
*
- * @param args Assertion configuration. See {@link #args()} for usage
examples.
- * @param expected The expected size/length.
- * @param actual The object to test. Must not be null.
- * @see #assertSize(int, Object)
- * @see #args()
+ * <p>Same as {@link #assertNotEmpty(Supplier, Object)} but without a
custom message.</p>
+ *
+ * @param value The object to test. Must not be null.
+ * @throws AssertionError if the object is null or empty
+ * @see #assertNotEmpty(Supplier, Object)
*/
- public static void assertSize(AssertionArgs args, int expected, Object
actual) {
- assertArgNotNull("args", args);
- assertNotNull(actual, "Value was null.");
- var size =
args.getBeanConverter().orElse(DEFAULT_CONVERTER).size(actual);
- assertEquals(expected, size, args.getMessage("Value not
expected size."));
+ public static void assertNotEmpty(Object value) {
+ assertNotEmpty(null, value);
}
+
/**
* Asserts that a collection-like object or string is not null and of
the specified size.
*
@@ -1054,30 +1187,32 @@ public class BctAssertions {
* <jsm>assertSize</jsm>(2, <jk>new</jk> String[]{<js>"x"</js>,
<js>"y"</js>});
* </p>
*
+ * @param message Optional custom error message supplier. If provided,
will be composed with the default assertion message.
* @param expected The expected size/length.
* @param actual The object to test. Must not be null.
* @throws AssertionError if the object is null or not the expected
size.
*/
- public static void assertSize(int expected, Object actual) {
- assertSize(args(), expected, actual);
+ public static void assertSize(Supplier<String> message, int expected,
Object actual) {
+ assertNotNull(actual, "Value was null.");
+ var size = getConverter().size(actual);
+ assertEquals(expected, size, composeMessage(message, "Value not
expected size."));
}
/**
- * Same as {@link #assertString(String, Object)} but with configurable
assertion behavior.
+ * Asserts that a collection-like object or string is not null and of
the specified size.
+ *
+ * <p>Same as {@link #assertSize(Supplier, int, Object)} but without a
custom message.</p>
*
- * @param args Assertion configuration. See {@link #args()} for usage
examples.
- * @param expected The expected string value.
+ * @param expected The expected size/length.
* @param actual The object to test. Must not be null.
- * @see #assertString(String, Object)
- * @see #args()
+ * @throws AssertionError if the object is null or not the expected
size.
+ * @see #assertSize(Supplier, int, Object)
*/
- public static void assertString(AssertionArgs args, String expected,
Object actual) {
- assertArgNotNull("args", args);
- assertNotNull(actual, "Value was null.");
-
- assertEquals(expected,
args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(actual),
args.getMessage());
+ public static void assertSize(int expected, Object actual) {
+ assertSize(null, expected, actual);
}
+
/**
* Asserts that an object's string representation exactly matches the
expected value.
*
@@ -1100,14 +1235,32 @@ public class BctAssertions {
* <jsm>assertString</jsm>(<js>"[red,green,blue]"</js>,
<jv>colors</jv>);
* </p>
*
+ * @param message Optional custom error message supplier. If provided,
will be composed with the default assertion message.
+ * @param expected The exact string that the actual object should
convert to
+ * @param actual The object to test. Must not be null.
+ * @throws AssertionError if the actual object is null or its string
representation doesn't exactly match expected
+ * @see #assertContains(Supplier, String, Object) for partial string
matching
+ * @see #assertMatchesGlob(Supplier, String, Object) for pattern-based
matching
+ */
+ public static void assertString(Supplier<String> message, String
expected, Object actual) {
+ assertNotNull(actual, "Value was null.");
+
+ var messageSupplier = message != null ? message : fs("");
+ assertEquals(expected, getConverter().stringify(actual),
messageSupplier);
+ }
+
+ /**
+ * Asserts that an object's string representation exactly matches the
expected value.
+ *
+ * <p>Same as {@link #assertString(Supplier, String, Object)} but
without a custom message.</p>
+ *
* @param expected The exact string that the actual object should
convert to
* @param actual The object to test. Must not be null.
* @throws AssertionError if the actual object is null or its string
representation doesn't exactly match expected
- * @see #assertContains(String, Object) for partial string matching
- * @see #assertMatchesGlob(String, Object) for pattern-based matching
+ * @see #assertString(Supplier, String, Object)
*/
public static void assertString(String expected, Object actual) {
- assertString(args(), expected, actual);
+ assertString(null, expected, actual);
}
private BctAssertions() {}
diff --git
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/package-info.java
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/package-info.java
index be615641b7..07c692fd46 100644
---
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/package-info.java
+++
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/package-info.java
@@ -33,15 +33,14 @@
*
* <h5 class='section'>Core Classes:</h5>
* <ul>
- * <li><b>{@link org.apache.juneau.junit.BctAssertions}:</b> Main assertion
methods for BCT</li>
- * <li><b>{@link org.apache.juneau.junit.BeanConverter}:</b> Interface for
object conversion and property access</li>
- * <li><b>{@link org.apache.juneau.junit.BasicBeanConverter}:</b> Default
implementation with extensible type handlers</li>
- * <li><b>{@link org.apache.juneau.junit.AssertionArgs}:</b> Configuration
for assertions with custom messages and converters</li>
+ * <li><b>{@link org.apache.juneau.junit.bct.BctAssertions}:</b> Main
assertion methods for BCT</li>
+ * <li><b>{@link org.apache.juneau.junit.bct.BeanConverter}:</b> Interface
for object conversion and property access</li>
+ * <li><b>{@link org.apache.juneau.junit.bct.BasicBeanConverter}:</b>
Default implementation with extensible type handlers</li>
* </ul>
*
* <h5 class='section'>Quick Start:</h5>
* <p class='bjava'>
- * <jk>import static</jk> com.sfdc.junit.bct.BctAssertions.*;
+ * <jk>import static</jk> org.apache.juneau.junit.bct.BctAssertions.*;
*
* <ja>@Test</ja>
* <jk>void</jk> testUser() {
@@ -57,7 +56,7 @@
*
* <h5 class='section'>Assertion Method Examples:</h5>
*
- * <h6 class='figure'>1. {@link
org.apache.juneau.junit.BctAssertions#assertBean(Object,String,String)
assertBean()}</h6>
+ * <h6 class='figure'>1. {@link
org.apache.juneau.junit.bct.BctAssertions#assertBean(Object,String,String)
assertBean()}</h6>
* <p>Tests object properties with support for nested syntax and collection
iteration.</p>
* <p class='bjava'>
* User <jv>user</jv> = <jk>new</jk> User(<js>"Bob"</js>, 30);
@@ -72,7 +71,7 @@
* <jsm>assertBean</jsm>(<jv>user</jv>, <js>"address{street,city}"</js>,
<js>"{456 Oak Ave,Denver}"</js>);
* </p>
*
- * <h6 class='figure'>2. {@link
org.apache.juneau.junit.BctAssertions#assertBeans(Object,String,String...)
assertBeans()}</h6>
+ * <h6 class='figure'>2. {@link
org.apache.juneau.junit.bct.BctAssertions#assertBeans(Object,String,String...)
assertBeans()}</h6>
* <p>Tests collections of objects by extracting and comparing specific
fields.</p>
* <p class='bjava'>
* List<User> <jv>users</jv> = Arrays.<jsm>asList</jsm>(
@@ -88,7 +87,7 @@
* <jsm>assertBeans</jsm>(<jv>users</jv>, <js>"name,age"</js>,
<js>"Alice,25"</js>, <js>"Bob,30"</js>, <js>"Carol,35"</js>);
* </p>
*
- * <h6 class='figure'>3. {@link
org.apache.juneau.junit.BctAssertions#assertMapped(Object,java.util.function.BiFunction,String,String)
assertMapped()}</h6>
+ * <h6 class='figure'>3. {@link
org.apache.juneau.junit.bct.BctAssertions#assertMapped(Object,java.util.function.BiFunction,String,String)
assertMapped()}</h6>
* <p>Tests custom property access using BiFunction for non-standard
objects.</p>
* <p class='bjava'>
* Map<String,Object> <jv>data</jv> = <jk>new</jk> HashMap<>();
@@ -99,7 +98,7 @@
* <jsm>assertMapped</jsm>(<jv>data</jv>, (obj, key) -> obj.get(key),
<js>"name,score"</js>, <js>"Alice,95"</js>);
* </p>
*
- * <h6 class='figure'>4. {@link
org.apache.juneau.junit.BctAssertions#assertList(Object,Object...)
assertList()}</h6>
+ * <h6 class='figure'>4. {@link
org.apache.juneau.junit.bct.BctAssertions#assertList(Object,Object...)
assertList()}</h6>
* <p>Tests list/collection elements with varargs for expected values.</p>
* <p class='bjava'>
* List<String> <jv>names</jv> =
Arrays.<jsm>asList</jsm>(<js>"Alice"</js>, <js>"Bob"</js>, <js>"Carol"</js>);
@@ -110,7 +109,7 @@
* <jsm>assertList</jsm>(<jv>colors</jv>, <js>"red"</js>, <js>"green"</js>,
<js>"blue"</js>);
* </p>
*
- * <h6 class='figure'>5. {@link
org.apache.juneau.junit.BctAssertions#assertContains(String,Object)
assertContains()}</h6>
+ * <h6 class='figure'>5. {@link
org.apache.juneau.junit.bct.BctAssertions#assertContains(String,Object)
assertContains()}</h6>
* <p>Tests that a string appears somewhere within the stringified object.</p>
* <p class='bjava'>
* User <jv>user</jv> = <jk>new</jk> User(<js>"Alice Smith"</js>, 25);
@@ -121,7 +120,7 @@
* <jsm>assertContains</jsm>(<js>"banana"</js>, <jv>items</jv>);
* </p>
*
- * <h6 class='figure'>6. {@link
org.apache.juneau.junit.BctAssertions#assertContainsAll(Object,String...)
assertContainsAll()}</h6>
+ * <h6 class='figure'>6. {@link
org.apache.juneau.junit.bct.BctAssertions#assertContainsAll(Object,String...)
assertContainsAll()}</h6>
* <p>Tests that all specified strings appear within the stringified
object.</p>
* <p class='bjava'>
* User <jv>user</jv> = <jk>new</jk> User(<js>"Alice Smith"</js>, 25);
@@ -132,7 +131,7 @@
* <jsm>assertContainsAll</jsm>(<jv>user</jv>, <js>"alice"</js>,
<js>"example.com"</js>);
* </p>
*
- * <h6 class='figure'>7. {@link
org.apache.juneau.junit.BctAssertions#assertEmpty(Object) assertEmpty()}</h6>
+ * <h6 class='figure'>7. {@link
org.apache.juneau.junit.bct.BctAssertions#assertEmpty(Object)
assertEmpty()}</h6>
* <p>Tests that collections, arrays, maps, or strings are empty.</p>
* <p class='bjava'>
* List<String> <jv>emptyList</jv> = <jk>new</jk> ArrayList<>();
@@ -147,7 +146,7 @@
* <jsm>assertEmpty</jsm>(<jv>emptyString</jv>);
* </p>
*
- * <h6 class='figure'>8. {@link
org.apache.juneau.junit.BctAssertions#assertNotEmpty(Object)
assertNotEmpty()}</h6>
+ * <h6 class='figure'>8. {@link
org.apache.juneau.junit.bct.BctAssertions#assertNotEmpty(Object)
assertNotEmpty()}</h6>
* <p>Tests that collections, arrays, maps, or strings are not empty.</p>
* <p class='bjava'>
* List<String> <jv>names</jv> =
Arrays.<jsm>asList</jsm>(<js>"Alice"</js>);
@@ -162,7 +161,7 @@
* <jsm>assertNotEmpty</jsm>(<jv>message</jv>);
* </p>
*
- * <h6 class='figure'>9. {@link
org.apache.juneau.junit.BctAssertions#assertSize(int,Object) assertSize()}</h6>
+ * <h6 class='figure'>9. {@link
org.apache.juneau.junit.bct.BctAssertions#assertSize(int,Object)
assertSize()}</h6>
* <p>Tests the size/length of collections, arrays, maps, or strings.</p>
* <p class='bjava'>
* List<String> <jv>names</jv> =
Arrays.<jsm>asList</jsm>(<js>"Alice"</js>, <js>"Bob"</js>, <js>"Carol"</js>);
@@ -177,7 +176,7 @@
* <jsm>assertSize</jsm>(5, <jv>message</jv>);
* </p>
*
- * <h6 class='figure'>10. {@link
org.apache.juneau.junit.BctAssertions#assertString(String,Object)
assertString()}</h6>
+ * <h6 class='figure'>10. {@link
org.apache.juneau.junit.bct.BctAssertions#assertString(String,Object)
assertString()}</h6>
* <p>Tests the string representation of an object using the configured
converter.</p>
* <p class='bjava'>
* User <jv>user</jv> = <jk>new</jk> User(<js>"Alice"</js>, 25);
@@ -190,7 +189,7 @@
* <jsm>assertString</jsm>(<js>"2021-01-01"</js>, <jv>date</jv>);
* </p>
*
- * <h6 class='figure'>11. {@link
org.apache.juneau.junit.BctAssertions#assertMatchesGlob(String,Object)
assertMatchesGlob()}</h6>
+ * <h6 class='figure'>11. {@link
org.apache.juneau.junit.bct.BctAssertions#assertMatchesGlob(String,Object)
assertMatchesGlob()}</h6>
* <p>Tests that the stringified object matches a glob-style pattern (* and ?
wildcards).</p>
* <p class='bjava'>
* User <jv>user</jv> = <jk>new</jk> User(<js>"Alice Smith"</js>, 25);
@@ -204,22 +203,42 @@
* <jsm>assertMatchesGlob</jsm>(<js>"User(name=Alice*, age=25)"</js>,
<jv>user</jv>);
* </p>
*
- * <h5 class='section'>Custom Configuration with {@link
org.apache.juneau.junit.AssertionArgs}:</h5>
- * <p>All assertion methods support custom configuration through {@link
org.apache.juneau.junit.AssertionArgs}:</p>
+ * <h5 class='section'>Custom Error Messages:</h5>
+ * <p>All assertion methods support custom error messages via a
<code>Supplier<String></code> parameter:</p>
* <p class='bjava'>
- * <jc>// Custom error message</jc>
- * <jsm>assertBean</jsm>(<jsm>args</jsm>(<js>"User validation
failed"</js>), <jv>user</jv>, <js>"name,age"</js>, <js>"Alice,25"</js>);
- *
- * <jc>// Custom converter configuration</jc>
- * AssertionArgs <jv>args</jv> = <jsm>args</jsm>()
- *
.setConverter(BasicBeanConverter.<jsm>builder</jsm>().<jsm>defaultSettings</jsm>()
- * .setSetting(<js>"nullValue"</js>, <js>"<empty>"</js>)
- * .build());
- * <jsm>assertBean</jsm>(<jv>args</jv>, <jv>user</jv>,
<js>"name,nickname"</js>, <js>"Alice,<empty>"</js>);
+ * <jc>// Simple custom message</jc>
+ * <jsm>assertBean</jsm>(() -> <js>"User validation failed"</js>,
<jv>user</jv>, <js>"name,age"</js>, <js>"Alice,25"</js>);
+ *
+ * <jc>// Formatted message using Utils.fs() for convenient message
suppliers with arguments</jc>
+ * <jsm>assertBean</jsm>(<jsm>fs</jsm>(<js>"User {0} validation
failed"</js>, <js>"Alice"</js>), <jv>user</jv>, <js>"name,age"</js>,
<js>"Alice,25"</js>);
+ * </p>
+ *
+ * <h5 class='section'>Customizing the Default Converter:</h5>
+ * <p>The default bean converter can be customized on a per-thread basis:</p>
+ * <p class='bjava'>
+ * <jc>// Set custom converter in @BeforeEach method</jc>
+ * <ja>@BeforeEach</ja>
+ * <jk>void</jk> <jsm>setUp</jsm>() {
+ * <jk>var</jk> <jv>converter</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>converter</jv>);
+ * }
+ *
+ * <jc>// All assertions now use the custom converter</jc>
+ * <jsm>assertBean</jsm>(<jv>user</jv>, <js>"birthDate"</js>,
<js>"2023-12-01"</js>);
+ *
+ * <jc>// Reset in @AfterEach method</jc>
+ * <ja>@AfterEach</ja>
+ * <jk>void</jk> <jsm>tearDown</jsm>() {
+ * BctAssertions.<jsm>resetConverter</jsm>();
+ * }
* </p>
*
- * @see org.apache.juneau.junit.BctAssertions
- * @see org.apache.juneau.junit.BeanConverter
- * @see org.apache.juneau.junit.BasicBeanConverter
+ * @see org.apache.juneau.junit.bct.BctAssertions
+ * @see org.apache.juneau.junit.bct.BeanConverter
+ * @see org.apache.juneau.junit.bct.BasicBeanConverter
+ * @see org.apache.juneau.common.utils.Utils#fs(String, Object...)
*/
package org.apache.juneau.junit.bct;
diff --git a/juneau-docs/docs/topics/07.01.00.JuneauBctBasics.md
b/juneau-docs/docs/topics/07.01.00.JuneauBctBasics.md
index 6bef635e40..6a4ee71d69 100644
--- a/juneau-docs/docs/topics/07.01.00.JuneauBctBasics.md
+++ b/juneau-docs/docs/topics/07.01.00.JuneauBctBasics.md
@@ -538,21 +538,37 @@ BCT provides several advanced configuration options for
customizing assertion be
### Custom Bean Converters
-```java
-// Create converter with custom formatting
-var converter = BasicBeanConverter.builder()
- .defaultSettings()
- .addStringifier(LocalDate.class, date ->
- date.format(DateTimeFormatter.ISO_LOCAL_DATE))
- .addStringifier(Money.class, money ->
- money.getAmount().toPlainString())
- .build();
+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.
+
+```java
+// Set custom converter in @BeforeEach method
+@BeforeEach
+void setUp() {
+ var converter = BasicBeanConverter.builder()
+ .defaultSettings()
+ .addStringifier(LocalDate.class, date ->
+ date.format(DateTimeFormatter.ISO_LOCAL_DATE))
+ .addStringifier(Money.class, money ->
+ money.getAmount().toPlainString())
+ .build();
+ BctAssertions.setConverter(converter);
+}
-// Use in assertions
-assertBean(args().setBeanConverter(converter),
- order, "date,total", "2023-12-01,99.99");
+// All assertions now use the custom converter
+@Test
+void testWithCustomConverter() {
+ assertBean(order, "date,total", "2023-12-01,99.99");
+}
+
+// Reset in @AfterEach method
+@AfterEach
+void tearDown() {
+ BctAssertions.resetConverter();
+}
```
+**Thread Safety:** The converter is stored per-thread, allowing parallel test
execution without cross-thread interference. Each test method running in
parallel will have its own converter instance.
+
### See Advanced Topics
For detailed information on extending and customizing BCT, see:
diff --git a/juneau-docs/docs/topics/07.01.01.Stringifiers.md
b/juneau-docs/docs/topics/07.01.01.Stringifiers.md
index 4c0fa4c017..ac587d3b58 100644
--- a/juneau-docs/docs/topics/07.01.01.Stringifiers.md
+++ b/juneau-docs/docs/topics/07.01.01.Stringifiers.md
@@ -110,8 +110,12 @@ var converter = BasicBeanConverter.builder()
.build();
// Usage in tests
-assertBean(args().setBeanConverter(converter),
- order, "total,customer", "$99.99,{John Smith <joh***@example.com>}");
+BctAssertions.setConverter(converter);
+try {
+ assertBean(order, "total,customer", "$99.99,{John Smith
<joh***@example.com>}");
+} finally {
+ BctAssertions.resetConverter();
+}
```
### Recursive Stringifier
@@ -152,8 +156,12 @@ var converter = BasicBeanConverter.builder()
.build();
// Use in assertions
-assertBean(args().setBeanConverter(converter),
- order, "date,total", "2023-12-01,99.99");
+BctAssertions.setConverter(converter);
+try {
+ assertBean(order, "date,total", "2023-12-01,99.99");
+} finally {
+ BctAssertions.resetConverter();
+}
```
## Best Practices
diff --git a/juneau-docs/docs/topics/07.01.02.Listifiers.md
b/juneau-docs/docs/topics/07.01.02.Listifiers.md
index dc24c89022..44b3de4c1b 100644
--- a/juneau-docs/docs/topics/07.01.02.Listifiers.md
+++ b/juneau-docs/docs/topics/07.01.02.Listifiers.md
@@ -147,25 +147,37 @@ private void collectDepthFirst(TreeNode node,
List<Object> result) {
```java
// Test paginated results
PaginatedResult<User> page = userService.getUsers(pageNumber);
-assertList(args().setBeanConverter(converter),
- page, "Alice", "Bob", "Charlie");
+BctAssertions.setConverter(converter);
+try {
+ assertList(page, "Alice", "Bob", "Charlie");
+} finally {
+ BctAssertions.resetConverter();
+}
// Test database results
ResultSet rs = statement.executeQuery("SELECT name FROM users");
-assertList(args().setBeanConverter(converter),
- rs,
- predicate(row -> ((Map)row).get("name").equals("Alice")),
- predicate(row -> ((Map)row).get("name").equals("Bob")));
+BctAssertions.setConverter(converter);
+try {
+ assertList(rs,
+ predicate(row -> ((Map)row).get("name").equals("Alice")),
+ predicate(row -> ((Map)row).get("name").equals("Bob")));
+} finally {
+ BctAssertions.resetConverter();
+}
```
### Combining with assertBean
```java
// Test collection properties
-assertBean(args().setBeanConverter(converter),
- paginatedResult,
- "items{#{name}},totalCount",
- "[{Alice},{Bob},{Charlie}],3");
+BctAssertions.setConverter(converter);
+try {
+ assertBean(paginatedResult,
+ "items{#{name}},totalCount",
+ "[{Alice},{Bob},{Charlie}],3");
+} finally {
+ BctAssertions.resetConverter();
+}
```
## Important Notes
diff --git a/juneau-docs/docs/topics/07.01.03.Swappers.md
b/juneau-docs/docs/topics/07.01.03.Swappers.md
index 9a570ec2fe..b18be8cb73 100644
--- a/juneau-docs/docs/topics/07.01.03.Swappers.md
+++ b/juneau-docs/docs/topics/07.01.03.Swappers.md
@@ -179,18 +179,20 @@ assertBean(futureOrder, "id,total", "456,99.99");
```java
// Test Result wrapper with custom swapper
Result<User> result = userService.createUser(userData);
-assertBean(args().setBeanConverter(converter),
- result, "name,email", "Alice,[email protected]");
-
-// Test validation results
-ValidationResult<Order> validation = orderValidator.validate(order);
-assertBean(args().setBeanConverter(converter),
- validation, "id,total", "123,99.99");
-
-// Test error case
-ValidationResult<Order> invalidValidation =
orderValidator.validate(invalidOrder);
-assertList(args().setBeanConverter(converter),
- invalidValidation, "Missing required field: customer", "Invalid
total: -10");
+BctAssertions.setConverter(converter);
+try {
+ assertBean(result, "name,email", "Alice,[email protected]");
+
+ // Test validation results
+ ValidationResult<Order> validation = orderValidator.validate(order);
+ assertBean(validation, "id,total", "123,99.99");
+
+ // Test error case
+ ValidationResult<Order> invalidValidation =
orderValidator.validate(invalidOrder);
+ assertList(invalidValidation, "Missing required field: customer", "Invalid
total: -10");
+} finally {
+ BctAssertions.resetConverter();
+}
```
### Testing Lazy Values
@@ -198,8 +200,12 @@ assertList(args().setBeanConverter(converter),
```java
// Test lazy computation
LazyValue<Report> lazyReport = new LazyValue<>(() -> generateReport());
-assertBean(args().setBeanConverter(converter),
- lazyReport, "title,itemCount", "Monthly Report,150");
+BctAssertions.setConverter(converter);
+try {
+ assertBean(lazyReport, "title,itemCount", "Monthly Report,150");
+} finally {
+ BctAssertions.resetConverter();
+}
// Swapper ensures the lazy value is evaluated before testing
```
diff --git a/juneau-docs/docs/topics/07.01.04.PropertyExtractors.md
b/juneau-docs/docs/topics/07.01.04.PropertyExtractors.md
index bf5a65ec24..4245e4423c 100644
--- a/juneau-docs/docs/topics/07.01.04.PropertyExtractors.md
+++ b/juneau-docs/docs/topics/07.01.04.PropertyExtractors.md
@@ -117,8 +117,12 @@ PropertyExtractor aliasExtractor = new PropertyExtractor()
{
};
// Usage
-assertBean(args().setBeanConverter(converter),
- user, "fname,lname,email", "John,Doe,[email protected]");
+BctAssertions.setConverter(converter);
+try {
+ assertBean(user, "fname,lname,email", "John,Doe,[email protected]");
+} finally {
+ BctAssertions.resetConverter();
+}
```
### Computed Property Extractor
@@ -148,9 +152,13 @@ PropertyExtractor computedExtractor = new
PropertyExtractor() {
};
// Usage
-assertBean(args().setBeanConverter(converter),
- user, "computed_fullName,computed_age,computed_initials",
- "John Doe,30,J.D.");
+BctAssertions.setConverter(converter);
+try {
+ assertBean(user, "computed_fullName,computed_age,computed_initials",
+ "John Doe,30,J.D.");
+} finally {
+ BctAssertions.resetConverter();
+}
```
## Complex Property Extraction Examples
@@ -203,8 +211,12 @@ PropertyExtractor privateFieldExtractor = new
PropertyExtractor() {
};
// Usage
-assertBean(args().setBeanConverter(converter),
- myBean, "_privateField1,_privateField2", "value1,value2");
+BctAssertions.setConverter(converter);
+try {
+ assertBean(myBean, "_privateField1,_privateField2", "value1,value2");
+} finally {
+ BctAssertions.resetConverter();
+}
```
### SQL ResultSet Extractor
@@ -265,9 +277,13 @@ PropertyExtractor configExtractor = new
PropertyExtractor() {
};
// Usage
-assertBean(args().setBeanConverter(converter),
- config, "database.host,database.port,app.name",
- "localhost,5432,MyApp");
+BctAssertions.setConverter(converter);
+try {
+ assertBean(config, "database.host,database.port,app.name",
+ "localhost,5432,MyApp");
+} finally {
+ BctAssertions.resetConverter();
+}
```
## Best Practices
@@ -432,28 +448,34 @@ PropertyExtractor fallbackExtractor = new
PropertyExtractor() {
```java
// Test database entity
DatabaseEntity entity = loadEntity(123);
-assertBean(args().setBeanConverter(converter),
- entity, "id,displayName,createdDate", "123,John Doe,2023-01-15");
-
-// Test configuration
-Configuration config = loadConfig();
-assertBean(args().setBeanConverter(converter),
- config, "database.host,database.port,app.timeout",
- "localhost,5432,30000");
-
-// Test computed properties
-User user = loadUser(456);
-assertBean(args().setBeanConverter(converter),
- user, "computed_fullName,computed_age", "Alice Smith,28");
+BctAssertions.setConverter(converter);
+try {
+ assertBean(entity, "id,displayName,createdDate", "123,John
Doe,2023-01-15");
+
+ // Test configuration
+ Configuration config = loadConfig();
+ assertBean(config, "database.host,database.port,app.timeout",
+ "localhost,5432,30000");
+
+ // Test computed properties
+ User user = loadUser(456);
+ assertBean(user, "computed_fullName,computed_age", "Alice Smith,28");
+} finally {
+ BctAssertions.resetConverter();
+}
```
### Combining with Other Features
```java
// Use property extractor with nested access
-assertBean(args().setBeanConverter(converter),
- order, "customer{computed_fullName},items{0{name}}",
- "{John Doe},{{Laptop}}");
+BctAssertions.setConverter(converter);
+try {
+ assertBean(order, "customer{computed_fullName},items{0{name}}",
+ "{John Doe},{{Laptop}}");
+} finally {
+ BctAssertions.resetConverter();
+}
```
## See Also
diff --git a/juneau-docs/docs/topics/07.01.05.CustomErrorMessages.md
b/juneau-docs/docs/topics/07.01.05.CustomErrorMessages.md
index ce21495894..d77ce1cb4f 100644
--- a/juneau-docs/docs/topics/07.01.05.CustomErrorMessages.md
+++ b/juneau-docs/docs/topics/07.01.05.CustomErrorMessages.md
@@ -9,47 +9,51 @@ Custom error messages allow you to provide contextual
information when BCT asser
## Overview
-BCT supports three types of custom error messages:
-- **Static messages** - Simple string messages
-- **Dynamic messages with placeholders** - Messages with variable substitution
-- **Supplier-based messages** - Lazy evaluation for expensive message
generation
+BCT supports custom error messages through a `Supplier<String>` parameter in
all assertion methods. This provides:
+- **Lazy evaluation** - Messages are only generated when assertions fail
+- **Format support** - Use `Utils.fs()` for convenient formatted messages with
arguments
+- **Flexible composition** - Combine custom messages with default assertion
messages
## Basic Usage
-### Static Messages
+### Simple Messages
```java
// Simple static message
-assertBean(args().setMessage("User validation failed"),
+assertBean(() -> "User validation failed",
user, "email", "[email protected]");
// More descriptive context
-assertBean(args().setMessage("Expected user to be active but was inactive"),
+assertBean(() -> "Expected user to be active but was inactive",
user, "isActive", "true");
// Test context information
-assertBean(args().setMessage("Order status check for order #12345"),
+assertBean(() -> "Order status check for order #12345",
order, "status", "PENDING");
```
-### Dynamic Messages with Placeholders
+### Formatted Messages with Utils.fs()
+
+The `Utils.fs()` method provides a convenient way to create message suppliers
with format arguments:
```java
+import static org.apache.juneau.common.utils.Utils.fs;
+
// Single placeholder
String testName = "validateUser";
-assertBean(args().setMessage("Test {0} failed", testName),
+assertBean(fs("Test {0} failed", testName),
result, "status", "SUCCESS");
// Multiple placeholders
String userName = "Alice";
int iteration = 5;
-assertBean(args().setMessage("User {0} validation failed on iteration {1}",
userName, iteration),
+assertBean(fs("User {0} validation failed on iteration {1}", userName,
iteration),
user, "isValid", "true");
// Contextual information
String orderId = "ORD-123";
String expectedStatus = "COMPLETED";
-assertBean(args().setMessage("Order {0} expected status {1}", orderId,
expectedStatus),
+assertBean(fs("Order {0} expected status {1}", orderId, expectedStatus),
order, "status", expectedStatus);
```
@@ -57,25 +61,25 @@ assertBean(args().setMessage("Order {0} expected status
{1}", orderId, expectedS
```java
// Lazy evaluation for expensive computation
-assertBean(args().setMessage(() -> "Test failed at " + Instant.now()),
+assertBean(() -> "Test failed at " + Instant.now(),
user, "lastLogin", expectedTime);
// Complex context information
-assertBean(args().setMessage(() -> {
+assertBean(() -> {
return String.format("Test failed in %s on thread %s",
Thread.currentThread().getName(),
Thread.currentThread().getId());
-}),
+},
result, "status", "SUCCESS");
// Conditional message generation
-assertBean(args().setMessage(() -> {
+assertBean(() -> {
if (isDebugMode()) {
return "Debug: Full stack trace available";
} else {
return "Test failed - enable debug for details";
}
-}),
+},
user, "email", "[email protected]");
```
@@ -84,13 +88,15 @@ assertBean(args().setMessage(() -> {
### Testing in Loops
```java
+import static org.apache.juneau.common.utils.Utils.fs;
+
@Test
void testMultipleOrders() {
List<Order> orders = getOrders();
for (int i = 0; i < orders.size(); i++) {
Order order = orders.get(i);
- assertBean(args().setMessage("Order validation failed at index {0}",
i),
+ assertBean(fs("Order validation failed at index {0}", i),
order, "status,total", "PENDING,99.99");
}
}
@@ -99,6 +105,8 @@ void testMultipleOrders() {
### Testing with Context Information
```java
+import static org.apache.juneau.common.utils.Utils.fs;
+
@Test
void testUsersByRole() {
Map<String, User> usersByRole = getUsersByRole();
@@ -107,7 +115,7 @@ void testUsersByRole() {
String role = entry.getKey();
User user = entry.getValue();
- assertBean(args().setMessage("User validation failed for role: {0}",
role),
+ assertBean(fs("User validation failed for role: {0}", role),
user, "role,isActive", role + ",true");
}
}
@@ -120,8 +128,8 @@ void testUsersByRole() {
void testOrderProcessing() {
Order order = processOrder();
- assertBean(args().setMessage(() ->
- String.format("Order processed at %s, validation failed",
LocalDateTime.now())),
+ assertBean(() ->
+ String.format("Order processed at %s, validation failed",
LocalDateTime.now()),
order, "status", "COMPLETED");
}
```
@@ -129,12 +137,14 @@ void testOrderProcessing() {
### Testing with Environment Information
```java
+import static org.apache.juneau.common.utils.Utils.fs;
+
@Test
void testConfiguration() {
Config config = loadConfig();
String environment = System.getProperty("env", "unknown");
- assertBean(args().setMessage("Config validation failed in environment:
{0}", environment),
+ assertBean(fs("Config validation failed in environment: {0}", environment),
config, "database.host,database.port", "localhost,5432");
}
```
@@ -170,11 +180,11 @@ void testConfiguration() {
```java
// Bad - expensive computation always executed
String heavyComputation = performExpensiveOperation();
-assertBean(args().setMessage("Test failed with: " + heavyComputation),
+assertBean(() -> "Test failed with: " + heavyComputation,
user, "status", "ACTIVE");
// Good - computation only happens on failure
-assertBean(args().setMessage(() -> "Test failed with: " +
performExpensiveOperation()),
+assertBean(() -> "Test failed with: " + performExpensiveOperation(),
user, "status", "ACTIVE");
```
@@ -183,27 +193,40 @@ assertBean(args().setMessage(() -> "Test failed with: " +
performExpensiveOperat
### Custom Messages with Custom Converters
```java
-var converter = BasicBeanConverter.builder()
- .defaultSettings()
- .addStringifier(LocalDate.class, date ->
- date.format(DateTimeFormatter.ISO_LOCAL_DATE))
- .build();
-
-assertBean(args()
- .setBeanConverter(converter)
- .setMessage("Date validation failed for user {0}", userId),
+import static org.apache.juneau.common.utils.Utils.fs;
+
+// Set custom converter in @BeforeEach
+@BeforeEach
+void setUp() {
+ var converter = BasicBeanConverter.builder()
+ .defaultSettings()
+ .addStringifier(LocalDate.class, date ->
+ date.format(DateTimeFormatter.ISO_LOCAL_DATE))
+ .build();
+ BctAssertions.setConverter(converter);
+}
+
+// Use custom message with the converter
+assertBean(fs("Date validation failed for user {0}", userId),
user, "birthDate", "1990-01-15");
+
+@AfterEach
+void tearDown() {
+ BctAssertions.resetConverter();
+}
```
### Custom Messages in Parameterized Tests
```java
+import static org.apache.juneau.common.utils.Utils.fs;
+
@ParameterizedTest
@ValueSource(strings = {"[email protected]", "[email protected]",
"[email protected]"})
void testUserEmails(String email) {
User user = findUserByEmail(email);
- assertBean(args().setMessage("User validation failed for email: {0}",
email),
+ assertBean(fs("User validation failed for email: {0}", email),
user, "email,isVerified", email + ",true");
}
```
@@ -211,13 +234,15 @@ void testUserEmails(String email) {
### Custom Messages with Dynamic Test Names
```java
+import static org.apache.juneau.common.utils.Utils.fs;
+
@TestFactory
Stream<DynamicTest> testOrders() {
return orders.stream()
.map(order -> DynamicTest.dynamicTest(
"Test Order #" + order.getId(),
() -> assertBean(
- args().setMessage("Order {0} validation failed",
order.getId()),
+ fs("Order {0} validation failed", order.getId()),
order, "status", "PENDING")));
}
```
@@ -251,7 +276,7 @@ void testOrderProcessing() {
processOrder(order);
// Validate with detailed context
- assertBean(args().setMessage(() ->
+ assertBean(() ->
String.format(
"Order validation failed:\n" +
" Order ID: %s\n" +
@@ -262,7 +287,7 @@ void testOrderProcessing() {
order.getCustomer().getName(),
Duration.between(order.getCreatedAt(), Instant.now()),
Thread.currentThread().getName()
- )),
+ ),
order, "status,isPaid,isShipped", "COMPLETED,true,true");
}
```
@@ -287,17 +312,17 @@ Actual: PENDING
void testUserLifecycle() {
// Creation phase
User user = createUser();
- assertBean(args().setMessage("User creation phase failed"),
+ assertBean(() -> "User creation phase failed",
user, "status", "NEW");
// Activation phase
activateUser(user);
- assertBean(args().setMessage("User activation phase failed"),
+ assertBean(() -> "User activation phase failed",
user, "status", "ACTIVE");
// Verification phase
verifyUser(user);
- assertBean(args().setMessage("User verification phase failed"),
+ assertBean(() -> "User verification phase failed",
user, "status,isVerified", "ACTIVE,true");
}
```
@@ -305,6 +330,8 @@ void testUserLifecycle() {
### Batch Processing
```java
+import static org.apache.juneau.common.utils.Utils.fs;
+
@Test
void testBatchProcessing() {
List<Order> orders = loadOrders();
@@ -313,7 +340,7 @@ void testBatchProcessing() {
Order order = orders.get(i);
processOrder(order);
- assertBean(args().setMessage(
+ assertBean(fs(
"Batch processing failed at index {0} of {1}, Order ID: {2}",
i, orders.size(), order.getId()),
order, "status", "PROCESSED");
@@ -324,12 +351,14 @@ void testBatchProcessing() {
### Conditional Testing
```java
+import static org.apache.juneau.common.utils.Utils.fs;
+
@Test
void testUserPermissions() {
User user = loadUser();
String expectedRole = user.isAdmin() ? "ADMIN" : "USER";
- assertBean(args().setMessage(
+ assertBean(fs(
"Permission check failed for {0} user (expected role: {1})",
user.isAdmin() ? "admin" : "regular",
expectedRole),
@@ -351,7 +380,7 @@ void testOrderIntegration() {
Result result = service.processOrder(order);
// Validate with full context
- assertBean(args().setMessage(() ->
+ assertBean(() ->
String.format(
"Integration test failed:\n" +
" Test ID: %s\n" +
@@ -362,7 +391,7 @@ void testOrderIntegration() {
service.getClass().getSimpleName(),
order.getId(),
result.getResponseTime()
- )),
+ ),
result, "status,errorCode", "SUCCESS,null");
}
```
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/AssertionArgs_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/AssertionArgs_Test.java
deleted file mode 100644
index 10d3479bbb..0000000000
---
a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/AssertionArgs_Test.java
+++ /dev/null
@@ -1,526 +0,0 @@
-/*
- * 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.common.utils.CollectionUtils.*;
-import static org.apache.juneau.junit.bct.BctAssertions.*;
-import static org.junit.jupiter.api.Assertions.*;
-
-import java.text.*;
-import java.time.*;
-import java.util.*;
-import java.util.function.*;
-
-import org.apache.juneau.*;
-import org.junit.jupiter.api.*;
-
-/**
- * Unit tests for {@link AssertionArgs}.
- *
- * <p>Tests the configuration and behavior of the assertion arguments class
including
- * bean converter customization, error message composition, and fluent API
functionality.</p>
- */
-class AssertionArgs_Test extends TestBase {
-
- // Test objects for assertions
- static class TestBean {
- private String name;
- private int age;
- private boolean active;
-
- public TestBean(String name, int age, boolean active) {
- this.name = name;
- this.age = age;
- this.active = active;
- }
-
- public String getName() { return name; }
- public int getAge() { return age; }
- public boolean isActive() { return active; }
- }
-
- static class CustomObject {
- private String value;
-
- public CustomObject(String value) {
- this.value = value;
- }
-
- public String getValue() { return value; }
-
- @Override
- public String toString() {
- return "CustomObject[" + value + "]";
- }
- }
-
- @Test
- void a01_defaultConstruction() {
- var args = new AssertionArgs();
-
- // Should have no custom converter
- assertEmpty(args.getBeanConverter());
-
- // Should have no custom message
- assertNull(args.getMessage());
- }
-
- @Test
- void a02_fluentAPIReturnsThis() {
- var args = new AssertionArgs();
- var mockConverter = createMockConverter();
-
- // Fluent methods should return the same instance
- assertSame(args, args.setBeanConverter(mockConverter));
- assertSame(args, args.setMessage("test message"));
- assertSame(args, args.setMessage(() -> "dynamic message"));
- }
-
- @Test
- void b01_beanConverterConfiguration() {
- var args = new AssertionArgs();
- var mockConverter = createMockConverter();
-
- // Initially empty
- assertEmpty(args.getBeanConverter());
-
- // Set converter
- args.setBeanConverter(mockConverter);
- assertTrue(args.getBeanConverter().isPresent());
- assertSame(mockConverter, args.getBeanConverter().get());
-
- // Set to null should clear
- args.setBeanConverter(null);
- assertEmpty(args.getBeanConverter());
- }
-
- @Test
- void b02_customConverterInAssertion() {
- // Create a mock custom converter for testing
- var customConverter = createCustomConverter();
-
- var args = args().setBeanConverter(customConverter);
- var obj = new TestBeanWithCustomObject("test", new
CustomObject("value"));
-
- // Should use custom converter for stringification
- assertBean(args, obj, "custom", "CUSTOM:value");
- }
-
- static class TestBeanWithCustomObject {
- private String name;
- private CustomObject custom;
-
- public TestBeanWithCustomObject(String name, CustomObject
custom) {
- this.name = name;
- this.custom = custom;
- }
-
- public String getName() { return name; }
- public CustomObject getCustom() { return custom; }
- }
-
- @Test
- void c01_messageSupplierConfiguration() {
- var args = new AssertionArgs();
-
- // Initially null
- assertNull(args.getMessage());
-
- // Set supplier
- Supplier<String> supplier = () -> "test message";
- args.setMessage(supplier);
- assertNotNull(args.getMessage());
- assertEquals("test message", args.getMessage().get());
-
- // Set different supplier
- args.setMessage(() -> "different message");
- assertEquals("different message", args.getMessage().get());
- }
-
- @Test
- void c02_parameterizedMessageConfiguration() {
- var args = new AssertionArgs();
-
- // Simple parameter substitution
- args.setMessage("Hello {0}", "World");
- assertEquals("Hello World", args.getMessage().get());
-
- // Multiple parameters
- args.setMessage("User {0} has {1} points", "John", 100);
- assertEquals("User John has 100 points",
args.getMessage().get());
-
- // Number formatting
- args.setMessage("Value: {0,number,#.##}", 123.456);
- assertEquals("Value: 123.46", args.getMessage().get());
- }
-
- @Test
- void c03_dynamicMessageSupplier() {
- var counter = new int[1]; // Mutable counter for testing
- var args = new AssertionArgs();
-
- args.setMessage(() -> "Call #" + (++counter[0]));
-
- // Each call should increment the counter
- assertEquals("Call #1", args.getMessage().get());
- assertEquals("Call #2", args.getMessage().get());
- assertEquals("Call #3", args.getMessage().get());
- }
-
- @Test
- void d01_messageCompositionWithoutCustomMessage() {
- var args = new AssertionArgs();
-
- // No custom message, should return assertion message as-is
- var composedMessage = args.getMessage("Bean assertion failed");
- assertEquals("Bean assertion failed", composedMessage.get());
-
- // With parameters
- var composedWithParams = args.getMessage("Element at index {0}
did not match", 5);
- assertEquals("Element at index 5 did not match",
composedWithParams.get());
- }
-
- @Test
- void d02_messageCompositionWithCustomMessage() {
- var args = new AssertionArgs();
- args.setMessage("User validation failed");
-
- // Should compose: custom + assertion
- var composedMessage = args.getMessage("Bean assertion failed");
- assertEquals("User validation failed, Caused by: Bean assertion
failed", composedMessage.get());
-
- // With parameters in assertion message
- var composedWithParams = args.getMessage("Element at index {0}
did not match", 3);
- assertEquals("User validation failed, Caused by: Element at
index 3 did not match", composedWithParams.get());
- }
-
- @Test
- void d03_messageCompositionWithParameterizedCustomMessage() {
- var args = new AssertionArgs();
- args.setMessage("Test {0} failed on iteration {1}",
"UserValidation", 42);
-
- var composedMessage = args.getMessage("Bean assertion failed");
- assertEquals("Test UserValidation failed on iteration 42,
Caused by: Bean assertion failed", composedMessage.get());
- }
-
- @Test
- void d04_messageCompositionWithDynamicCustomMessage() {
- var timestamp = Instant.now().toString();
- var args = new AssertionArgs();
- args.setMessage(() -> "Test failed at " + timestamp);
-
- var composedMessage = args.getMessage("Bean assertion failed");
- assertEquals("Test failed at " + timestamp + ", Caused by: Bean
assertion failed", composedMessage.get());
- }
-
- @Test
- void e01_fluentConfigurationChaining() {
- var converter = createMockConverter();
-
- // Chain multiple configurations
- var args = new AssertionArgs()
- .setBeanConverter(converter)
- .setMessage("Integration test failed for module {0}",
"AuthModule");
-
- // Verify both configurations applied
- assertTrue(args.getBeanConverter().isPresent());
- assertSame(converter, args.getBeanConverter().get());
- assertEquals("Integration test failed for module AuthModule",
args.getMessage().get());
- }
-
- @Test
- void e02_configurationOverwriting() {
- var args = new AssertionArgs();
- var converter1 = createMockConverter();
- var converter2 = createMockConverter();
-
- // Set initial values
- args.setBeanConverter(converter1).setMessage("First message");
-
- // Overwrite with new values
- args.setBeanConverter(converter2).setMessage("Second message");
-
- // Should have latest values
- assertSame(converter2, args.getBeanConverter().get());
- assertEquals("Second message", args.getMessage().get());
- }
-
- @Test
- void f01_integrationWithAssertBean() {
- var bean = new TestBean("John", 30, true);
- var args = args().setMessage("User test failed");
-
- // Should work with custom message
- assertBean(args, bean, "name,age,active", "John,30,true");
-
- // Test assertion failure message composition
- var exception = assertThrows(AssertionError.class, () -> {
- assertBean(args, bean, "name", "Jane");
- });
-
- assertTrue(exception.getMessage().contains("User test failed"));
- assertTrue(exception.getMessage().contains("Caused by:"));
- }
-
- @Test
- void f02_integrationWithAssertBeans() {
- var beans = l(
- new TestBean("Alice", 25, true),
- new TestBean("Bob", 35, false)
- );
- var args = args().setMessage("Batch validation failed");
-
- // Should work with custom message
- assertBeans(args, beans, "name,age", "Alice,25", "Bob,35");
-
- // Test assertion failure message composition
- var exception = assertThrows(AssertionError.class, () -> {
- assertBeans(args, beans, "name", "Charlie", "David");
- });
-
- assertTrue(exception.getMessage().contains("Batch validation
failed"));
- assertTrue(exception.getMessage().contains("Caused by:"));
- }
-
- @Test
- void f03_integrationWithAssertList() {
- var list = l("apple", "banana", "cherry");
- var args = args().setMessage("List validation failed");
-
- // Should work with custom message
- assertList(args, list, "apple", "banana", "cherry");
-
- // Test assertion failure message composition
- var exception = assertThrows(AssertionError.class, () -> {
- assertList(args, list, "orange", "banana", "cherry");
- });
-
- assertTrue(exception.getMessage().contains("List validation
failed"));
- assertTrue(exception.getMessage().contains("Caused by:"));
- }
-
- @Test
- void g01_edgeCaseNullValues() {
- var args = new AssertionArgs();
-
- // Null converter should work
- args.setBeanConverter(null);
- assertEmpty(args.getBeanConverter());
-
- // Null message supplier should work
- args.setMessage((Supplier<String>) null);
- assertNull(args.getMessage());
- }
-
- @Test
- void g02_edgeCaseEmptyMessages() {
- var args = new AssertionArgs();
-
- // Empty string message
- args.setMessage("");
- assertEquals("", args.getMessage().get());
-
- // Empty supplier result
- args.setMessage(() -> "");
- assertEquals("", args.getMessage().get());
-
- // Composition with empty custom message
- var composedMessage = args.getMessage("Bean assertion failed");
- assertEquals(", Caused by: Bean assertion failed",
composedMessage.get());
- }
-
- @Test
- void g03_edgeCaseComplexParameterFormatting() {
- var args = new AssertionArgs();
- var date = new Date();
-
- // Date formatting
- args.setMessage("Test executed on {0,date,short}", date);
- var expectedDatePart =
DateFormat.getDateInstance(DateFormat.SHORT).format(date);
- assertTrue(args.getMessage().get().contains(expectedDatePart));
-
- // Complex number formatting
- args.setMessage("Processing {0,number,percent} complete", 0.85);
- assertTrue(args.getMessage().get().contains("85%"));
- }
-
- @Test
- void h01_threadSafetyDocumentationCompliance() {
- // This test documents that AssertionArgs is NOT thread-safe
- // Each thread should create its own instance
-
- var sharedArgs = new AssertionArgs();
- var results = Collections.synchronizedList(list());
-
- // Simulate multiple threads modifying the same instance
- var threads = new Thread[5];
- for (var i = 0; i < threads.length; i++) {
- final int threadId = i;
- threads[i] = new Thread(() -> {
- sharedArgs.setMessage("Thread " + threadId + "
message");
- // Small delay to increase chance of race
condition
- try { Thread.sleep(1); } catch
(InterruptedException e) {}
- results.add(sharedArgs.getMessage().get());
- });
- }
-
- // Start all threads
- for (var thread : threads) {
- thread.start();
- }
-
- // Wait for completion
- for (var thread : threads) {
- try { thread.join(); } catch (InterruptedException e) {}
- }
-
- // Due to race conditions, we may not get the expected messages
- // This demonstrates why each test should create its own
instance
- assertSize(5, results);
- // Note: We don't assert specific values due to race conditions
- }
-
- @Test
- void h02_recommendedUsagePattern() {
- // Demonstrate the recommended pattern: create new instance per
test
-
- // Test 1: User validation
- var userArgs = args().setMessage("User validation test");
- var user = new TestBean("Alice", 25, true);
- assertBean(userArgs, user, "name,active", "Alice,true");
-
- // Test 2: Product validation (separate instance)
- var productArgs = args().setMessage("Product validation test");
- var products = l("Laptop", "Phone", "Tablet");
- assertList(productArgs, products, "Laptop", "Phone", "Tablet");
-
- // Each test has its own configuration without interference
- assertEquals("User validation test",
userArgs.getMessage().get());
- assertEquals("Product validation test",
productArgs.getMessage().get());
- }
-
- // Helper method to create a mock converter for testing
- private static BeanConverter createMockConverter() {
- return new BeanConverter() {
- @Override
- public String stringify(Object o) {
- return String.valueOf(o);
- }
-
- @Override
- public List<Object> listify(Object o) {
- if (o instanceof List) return (List<Object>) o;
- return l(o);
- }
-
- @Override
- public int size(Object o) {
- if (o instanceof List o2) return o2.size();
- if (o instanceof String o3) return o3.length();
- return 1;
- }
-
- @Override
- public boolean canListify(Object o) {
- return true;
- }
-
- @Override
- public Object swap(Object o) {
- return o;
- }
-
- @Override
- public Object getProperty(Object object, String name) {
- // Simple mock implementation
- if ("name".equals(name) && object instanceof
TestBean object2) {
- return object2.getName();
- }
- if ("custom".equals(name) && object instanceof
TestBeanWithCustomObject object3) {
- return object3.getCustom();
- }
- return null;
- }
-
- @Override
- public <T> T getSetting(String key, T defaultValue) {
- return defaultValue;
- }
-
- @Override
- public String getNested(Object o, NestedTokenizer.Token
token) {
- var propValue = getProperty(o,
token.getValue());
- return stringify(propValue);
- }
- };
- }
-
- // Helper method to create a custom converter for testing
- private static BeanConverter createCustomConverter() {
- return new BeanConverter() {
- @Override
- public String stringify(Object o) {
- if (o instanceof CustomObject o2) {
- return "CUSTOM:" + o2.getValue();
- }
- return String.valueOf(o);
- }
-
- @Override
- public List<Object> listify(Object o) {
- if (o instanceof List) return (List<Object>) o;
- return l(o);
- }
-
- @Override
- public int size(Object o) {
- if (o instanceof List o2) return o2.size();
- if (o instanceof String o3) return o3.length();
- return 1;
- }
-
- @Override
- public boolean canListify(Object o) {
- return true;
- }
-
- @Override
- public Object swap(Object o) {
- return o;
- }
-
- @Override
- public Object getProperty(Object object, String name) {
- if ("custom".equals(name) && object instanceof
TestBeanWithCustomObject object2) {
- return object2.getCustom();
- }
- return null;
- }
-
- @Override
- public <T> T getSetting(String key, T defaultValue) {
- return defaultValue;
- }
-
- @Override
- public String getNested(Object o, NestedTokenizer.Token
token) {
- var propValue = getProperty(o,
token.getValue());
- return stringify(propValue);
- }
- };
- }
-}
\ No newline at end of file
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 9916611af1..b0b616474f 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
@@ -38,22 +38,6 @@ import org.opentest4j.*;
*/
class BctAssertions_Test extends TestBase {
- //
====================================================================================================
- // AssertionArgs Tests
- //
====================================================================================================
-
- @Nested
- class A_assertionArgs extends TestBase {
-
- @Test
- void a01_args() {
- var args = args();
- assertNotNull(args);
- assertEmpty(args.getBeanConverter());
- assertNull(args.getMessage());
- }
- }
-
//
====================================================================================================
// Bean Property Tests
//
====================================================================================================
@@ -70,11 +54,10 @@ class BctAssertions_Test extends TestBase {
}
@Test
- void b02_withCustomArgs() {
+ void b02_withCustomMessage() {
var person = new TestPerson("Bob", 30);
- var args = args().setMessage("Custom message");
- assertDoesNotThrow(() -> assertBean(args, person,
"name", "Bob"));
+ assertDoesNotThrow(() -> assertBean(() -> "Custom
message", person, "name", "Bob"));
}
@Test
@@ -95,9 +78,8 @@ class BctAssertions_Test extends TestBase {
@Test
void b05_customMessage() {
var person = new TestPerson("Charlie", 35);
- var args = args().setMessage("Custom error message");
- var e = assertThrows(AssertionFailedError.class, () ->
assertBean(args, person, "name", "Wrong"));
+ var e = assertThrows(AssertionFailedError.class, () ->
assertBean(() -> "Custom error message", person, "name", "Wrong"));
assertContains("Custom error message", e.getMessage());
}
}
@@ -117,11 +99,10 @@ class BctAssertions_Test extends TestBase {
}
@Test
- void c02_withCustomArgs() {
+ void c02_withCustomMessage() {
var people = l(new TestPerson("Charlie", 35));
- var args = args().setMessage("Custom beans message");
- assertDoesNotThrow(() -> assertBeans(args, people,
"name", "Charlie"));
+ assertDoesNotThrow(() -> assertBeans(() -> "Custom
beans message", people, "name", "Charlie"));
}
@Test
@@ -168,12 +149,11 @@ class BctAssertions_Test extends TestBase {
}
@Test
- void d02_withCustomArgs() {
+ void d02_withCustomMessage() {
var person = new TestPerson("Henry", 45);
- var args = args().setMessage("Custom mapped message");
BiFunction<TestPerson,String,Object> mapper = (p, prop)
-> p.getName();
- assertDoesNotThrow(() -> assertMapped(args, person,
mapper, "name", "Henry"));
+ assertDoesNotThrow(() -> assertMapped(() -> "Custom
mapped message", person, mapper, "name", "Henry"));
}
@Test
@@ -200,10 +180,8 @@ class BctAssertions_Test extends TestBase {
}
@Test
- void e02_withCustomArgs() {
- var args = args().setMessage("Custom contains message");
-
- assertDoesNotThrow(() -> assertContains(args, "Test",
"Test String"));
+ void e02_withCustomMessage() {
+ assertDoesNotThrow(() -> assertContains(() -> "Custom
contains message", "Test", "Test String"));
}
@Test
@@ -232,10 +210,8 @@ class BctAssertions_Test extends TestBase {
}
@Test
- void f02_withCustomArgs() {
- var args = args().setMessage("Custom contains all
message");
-
- assertDoesNotThrow(() -> assertContainsAll(args,
(Object)"Testing", "Test"));
+ void f02_withCustomMessage() {
+ assertDoesNotThrow(() -> assertContainsAll(() ->
"Custom contains all message", (Object)"Testing", "Test"));
}
@Test
@@ -302,10 +278,8 @@ class BctAssertions_Test extends TestBase {
}
@Test
- void g02_withCustomArgs() {
- var args = args().setMessage("Custom empty message");
-
- assertDoesNotThrow(() -> assertEmpty(args, l()));
+ void g02_withCustomMessage() {
+ assertDoesNotThrow(() -> assertEmpty(() -> "Custom
empty message", l()));
}
@Test
@@ -334,10 +308,8 @@ class BctAssertions_Test extends TestBase {
}
@Test
- void h02_withCustomArgs() {
- var args = args().setMessage("Custom list message");
-
- assertDoesNotThrow(() -> assertList(args, l(1, 2), 1,
2));
+ void h02_withCustomMessage() {
+ assertDoesNotThrow(() -> assertList(() -> "Custom list
message", l(1, 2), 1, 2));
}
@Test
@@ -354,18 +326,18 @@ class BctAssertions_Test extends TestBase {
@Test
void h05_nullValue() {
- var e = assertThrows(IllegalArgumentException.class, ()
-> assertList(null, "test"));
+ Object nullList = null;
+ var e = assertThrows(IllegalArgumentException.class, ()
-> assertList(nullList, "test"));
assertContains("cannot be null", e.getMessage());
}
@Test
void h06_predicateValidation() {
// Test lines 765-766: Predicate-based element
validation
- var args = args().setMessage("Custom predicate
message");
var numbers = l(1, 2, 3, 4, 5);
// Test successful predicate validation
- assertDoesNotThrow(() -> assertList(args, numbers,
(Predicate<Integer>)x -> x == 1, // First element should equal 1
+ assertDoesNotThrow(() -> assertList(() -> "Custom
predicate message", numbers, (Predicate<Integer>)x -> x == 1, // First
element should equal 1
(Predicate<Integer>)x -> x > 1, // Second
element should be > 1
"3", // Third
element as string
(Predicate<Integer>)x -> x % 2 == 0, // Fourth
element should be even
@@ -374,7 +346,7 @@ class BctAssertions_Test extends TestBase {
// Test failed predicate validation - use single
element list to avoid length mismatch
var singleNumber = l(1);
- var e = assertThrows(AssertionFailedError.class, () ->
assertList(args, singleNumber, (Predicate<Integer>)x -> x == 99)); // Should
fail
+ var e = assertThrows(AssertionFailedError.class, () ->
assertList(() -> "Custom predicate message", singleNumber,
(Predicate<Integer>)x -> x == 99)); // Should fail
assertContains("Element at index 0 did not pass
predicate", e.getMessage());
assertContains("actual: <1>", e.getMessage());
}
@@ -428,9 +400,8 @@ class BctAssertions_Test extends TestBase {
}
@Test
- void h02_withCustomArgs() {
- var args = args().setMessage("Custom map message");
- assertDoesNotThrow(() -> assertMap(args, m("key",
"value"), "key=value"));
+ void h02_withCustomMessage() {
+ assertDoesNotThrow(() -> assertMap(() -> "Custom map
message", m("key", "value"), "key=value"));
}
@Test
@@ -447,23 +418,23 @@ class BctAssertions_Test extends TestBase {
@Test
void h05_nullValue() {
- var e = assertThrows(AssertionFailedError.class, () ->
assertMap(null, "test"));
- assertContains("Value was null", e.getMessage());
+ Map<?,?> nullMap = null;
+ var e = assertThrows(IllegalArgumentException.class, ()
-> assertMap(nullMap, "test"));
+ assertContains("cannot be null", e.getMessage());
}
@Test
void h06_predicateValidation() {
// Test predicate-based map entry validation
- var args = args().setMessage("Custom predicate
message");
var map = m("count", 42, "enabled", true);
// Test successful predicate validation
- assertDoesNotThrow(() -> assertMap(args, map,
(Predicate<Map.Entry<String,Object>>)entry -> entry.getKey().equals("count") &&
entry.getValue().equals(42),
+ assertDoesNotThrow(() -> assertMap(() -> "Custom
predicate message", map, (Predicate<Map.Entry<String,Object>>)entry ->
entry.getKey().equals("count") && entry.getValue().equals(42),
(Predicate<Map.Entry<String,Object>>)entry ->
entry.getKey().equals("enabled") && entry.getValue().equals(true)));
// Test failed predicate validation
var singleEntryMap = m("count", 1);
- var e = assertThrows(AssertionFailedError.class, () ->
assertMap(args, singleEntryMap, (Predicate<Map.Entry<String,Object>>)entry ->
entry.getValue().equals(99))); // Should fail
+ var e = assertThrows(AssertionFailedError.class, () ->
assertMap(() -> "Custom predicate message", singleEntryMap,
(Predicate<Map.Entry<String,Object>>)entry -> entry.getValue().equals(99))); //
Should fail
assertContains("Element at index 0 did not pass
predicate", e.getMessage());
assertContains("actual: <count=1>", e.getMessage());
}
@@ -566,10 +537,8 @@ class BctAssertions_Test extends TestBase {
}
@Test
- void i02_withCustomArgs() {
- var args = args().setMessage("Custom not empty
message");
-
- assertDoesNotThrow(() -> assertNotEmpty(args,
l("content")));
+ void i02_withCustomMessage() {
+ assertDoesNotThrow(() -> assertNotEmpty(() -> "Custom
not empty message", l("content")));
}
@Test
@@ -599,10 +568,8 @@ class BctAssertions_Test extends TestBase {
}
@Test
- void j02_withCustomArgs() {
- var args = args().setMessage("Custom size message");
-
- assertDoesNotThrow(() -> assertSize(args, 2, l("a",
"b")));
+ void j02_withCustomMessage() {
+ assertDoesNotThrow(() -> assertSize(() -> "Custom size
message", 2, l("a", "b")));
}
@Test
@@ -631,10 +598,8 @@ class BctAssertions_Test extends TestBase {
}
@Test
- void k02_withCustomArgs() {
- var args = args().setMessage("Custom string message");
-
- assertDoesNotThrow(() -> assertString(args, "test",
"test"));
+ void k02_withCustomMessage() {
+ assertDoesNotThrow(() -> assertString(() -> "Custom
string message", "test", "test"));
}
@Test
@@ -665,10 +630,8 @@ class BctAssertions_Test extends TestBase {
}
@Test
- void l02_withCustomArgs() {
- var args = args().setMessage("Custom glob message");
-
- assertDoesNotThrow(() -> assertMatchesGlob(args,
"test*", "testing"));
+ void l02_withCustomMessage() {
+ assertDoesNotThrow(() -> assertMatchesGlob(() ->
"Custom glob message", "test*", "testing"));
}
@Test