Custom Error Messages
Custom error messages allow you to provide contextual information when BCT assertions fail. This makes test failures more informative and helps identify issues faster during debugging.
Overview
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
Simple Messages
// Simple static message
assertBean(() -> "User validation failed",
user, "email", "john@example.com");
// More descriptive context
assertBean(() -> "Expected user to be active but was inactive",
user, "isActive", "true");
// Test context information
assertBean(() -> "Order status check for order #12345",
order, "status", "PENDING");
Formatted Messages with Utils.fs()
The Utils.fs() method provides a convenient way to create message suppliers with format arguments:
import static org.apache.juneau.commons.utils.Utils.fs;
// Single placeholder
String testName = "validateUser";
assertBean(fs("Test {0} failed", testName),
result, "status", "SUCCESS");
// Multiple placeholders
String userName = "Alice";
int iteration = 5;
assertBean(fs("User {0} validation failed on iteration {1}", userName, iteration),
user, "isValid", "true");
// Contextual information
String orderId = "ORD-123";
String expectedStatus = "COMPLETED";
assertBean(fs("Order {0} expected status {1}", orderId, expectedStatus),
order, "status", expectedStatus);
Supplier-Based Messages
// Lazy evaluation for expensive computation
assertBean(() -> "Test failed at " + Instant.now(),
user, "lastLogin", expectedTime);
// Complex context information
assertBean(() -> {
return String.format("Test failed in %s on thread %s",
Thread.currentThread().getName(),
Thread.currentThread().getId());
},
result, "status", "SUCCESS");
// Conditional message generation
assertBean(() -> {
if (isDebugMode()) {
return "Debug: Full stack trace available";
} else {
return "Test failed - enable debug for details";
}
},
user, "email", "john@example.com");
Advanced Usage Examples
Testing in Loops
import static org.apache.juneau.commons.utils.Utils.fs;
@Test
void testMultipleOrders() {
List<Order> orders = getOrders();
for (int i = 0; i < orders.size(); i++) {
Order order = orders.get(i);
assertBean(fs("Order validation failed at index {0}", i),
order, "status,total", "PENDING,99.99");
}
}
Testing with Context Information
import static org.apache.juneau.commons.utils.Utils.fs;
@Test
void testUsersByRole() {
Map<String, User> usersByRole = getUsersByRole();
for (Map.Entry<String, User> entry : usersByRole.entrySet()) {
String role = entry.getKey();
User user = entry.getValue();
assertBean(fs("User validation failed for role: {0}", role),
user, "role,isActive", role + ",true");
}
}
Testing with Timestamps
@Test
void testOrderProcessing() {
Order order = processOrder();
assertBean(() ->
String.format("Order processed at %s, validation failed", LocalDateTime.now()),
order, "status", "COMPLETED");
}
Testing with Environment Information
import static org.apache.juneau.commons.utils.Utils.fs;
@Test
void testConfiguration() {
Config config = loadConfig();
String environment = System.getProperty("env", "unknown");
assertBean(fs("Config validation failed in environment: {0}", environment),
config, "database.host,database.port", "localhost,5432");
}
Best Practices
When to Use Custom Messages
Use custom messages when:
- Testing in loops or with multiple iterations
- Test failures need additional context
- Debugging complex test scenarios
- Testing with dynamic or generated data
- Running tests in different environments
- Testing concurrent or parallel scenarios
Avoid custom messages when:
- The test is simple and self-explanatory
- The assertion failure message is already clear
- Adding message overhead provides no value
Message Guidelines
- Be specific: Include relevant context (IDs, names, indices)
- Be concise: Keep messages short but informative
- Include values: Add expected/actual values when helpful
- Add context: Include loop indices, test phases, environment info
- Use suppliers for expensive operations: Defer message generation until needed
- Avoid sensitive data: Don't include passwords, tokens, or PII in messages
Performance Considerations
// Bad - expensive computation always executed
String heavyComputation = performExpensiveOperation();
assertBean(() -> "Test failed with: " + heavyComputation,
user, "status", "ACTIVE");
// Good - computation only happens on failure
assertBean(() -> "Test failed with: " + performExpensiveOperation(),
user, "status", "ACTIVE");
Combining with Other Features
Custom Messages with Custom Converters
import static org.apache.juneau.commons.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
import static org.apache.juneau.commons.utils.Utils.fs;
@ParameterizedTest
@ValueSource(strings = {"alice@example.com", "bob@example.com", "carol@example.com"})
void testUserEmails(String email) {
User user = findUserByEmail(email);
assertBean(fs("User validation failed for email: {0}", email),
user, "email,isVerified", email + ",true");
}
Custom Messages with Dynamic Test Names
import static org.apache.juneau.commons.utils.Utils.fs;
@TestFactory
Stream<DynamicTest> testOrders() {
return orders.stream()
.map(order -> DynamicTest.dynamicTest(
"Test Order #" + order.getId(),
() -> assertBean(
fs("Order {0} validation failed", order.getId()),
order, "status", "PENDING")));
}
Error Message Examples
Before (Without Custom Message)
org.opentest4j.AssertionFailedError:
Expected: COMPLETED
Actual: PENDING
After (With Custom Message)
org.opentest4j.AssertionFailedError: Order ORD-123 expected status COMPLETED
Expected: COMPLETED
Actual: PENDING
Advanced Example
@Test
void testOrderProcessing() {
Order order = createOrder();
// Process order
processOrder(order);
// Validate with detailed context
assertBean(() ->
String.format(
"Order validation failed:\n" +
" Order ID: %s\n" +
" Customer: %s\n" +
" Processing Time: %s\n" +
" Thread: %s",
order.getId(),
order.getCustomer().getName(),
Duration.between(order.getCreatedAt(), Instant.now()),
Thread.currentThread().getName()
),
order, "status,isPaid,isShipped", "COMPLETED,true,true");
}
Output on failure:
org.opentest4j.AssertionFailedError: Order validation failed:
Order ID: ORD-123
Customer: John Doe
Processing Time: PT2.5S
Thread: main
Expected: COMPLETED
Actual: PENDING
Common Patterns
Test Phase Identification
@Test
void testUserLifecycle() {
// Creation phase
User user = createUser();
assertBean(() -> "User creation phase failed",
user, "status", "NEW");
// Activation phase
activateUser(user);
assertBean(() -> "User activation phase failed",
user, "status", "ACTIVE");
// Verification phase
verifyUser(user);
assertBean(() -> "User verification phase failed",
user, "status,isVerified", "ACTIVE,true");
}
Batch Processing
import static org.apache.juneau.commons.utils.Utils.fs;
@Test
void testBatchProcessing() {
List<Order> orders = loadOrders();
for (int i = 0; i < orders.size(); i++) {
Order order = orders.get(i);
processOrder(order);
assertBean(fs(
"Batch processing failed at index {0} of {1}, Order ID: {2}",
i, orders.size(), order.getId()),
order, "status", "PROCESSED");
}
}
Conditional Testing
import static org.apache.juneau.commons.utils.Utils.fs;
@Test
void testUserPermissions() {
User user = loadUser();
String expectedRole = user.isAdmin() ? "ADMIN" : "USER";
assertBean(fs(
"Permission check failed for {0} user (expected role: {1})",
user.isAdmin() ? "admin" : "regular",
expectedRole),
user, "role,hasAccess", expectedRole + ",true");
}
Integration Test Context
@Test
void testOrderIntegration() {
// Create test data
Order order = createTestOrder();
String testId = UUID.randomUUID().toString();
// Call external service
ExternalService service = getExternalService();
Result result = service.processOrder(order);
// Validate with full context
assertBean(() ->
String.format(
"Integration test failed:\n" +
" Test ID: %s\n" +
" Service: %s\n" +
" Order ID: %s\n" +
" Response Time: %dms",
testId,
service.getClass().getSimpleName(),
order.getId(),
result.getResponseTime()
),
result, "status,errorCode", "SUCCESS,null");
}
See Also
- juneau-bct Basics - Main BCT documentation
- Customization - Configuration via @BctConfig and BctConfiguration
- Stringifiers - Converting objects to strings
- Listifiers - Converting collection-like objects to lists
- Swappers - Transforming objects before processing
- PropertyExtractors - Custom property access logic
Share feedback or follow-up questions for this page directly through GitHub.