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 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
Basic Usage
Static Messages
// Simple static message
assertBean(args().setMessage("User validation failed"),
user, "email", "john@example.com");
// More descriptive context
assertBean(args().setMessage("Expected user to be active but was inactive"),
user, "isActive", "true");
// Test context information
assertBean(args().setMessage("Order status check for order #12345"),
order, "status", "PENDING");
Dynamic Messages with Placeholders
// Single placeholder
String testName = "validateUser";
assertBean(args().setMessage("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),
user, "isValid", "true");
// Contextual information
String orderId = "ORD-123";
String expectedStatus = "COMPLETED";
assertBean(args().setMessage("Order {0} expected status {1}", orderId, expectedStatus),
order, "status", expectedStatus);
Supplier-Based Messages
// Lazy evaluation for expensive computation
assertBean(args().setMessage(() -> "Test failed at " + Instant.now()),
user, "lastLogin", expectedTime);
// Complex context information
assertBean(args().setMessage(() -> {
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(() -> {
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
@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),
order, "status,total", "PENDING,99.99");
}
}
Testing with Context Information
@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(args().setMessage("User validation failed for role: {0}", role),
user, "role,isActive", role + ",true");
}
}
Testing with Timestamps
@Test
void testOrderProcessing() {
Order order = processOrder();
assertBean(args().setMessage(() ->
String.format("Order processed at %s, validation failed", LocalDateTime.now())),
order, "status", "COMPLETED");
}
Testing with Environment Information
@Test
void testConfiguration() {
Config config = loadConfig();
String environment = System.getProperty("env", "unknown");
assertBean(args().setMessage("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(args().setMessage("Test failed with: " + heavyComputation),
user, "status", "ACTIVE");
// Good - computation only happens on failure
assertBean(args().setMessage(() -> "Test failed with: " + performExpensiveOperation()),
user, "status", "ACTIVE");
Combining with Other Features
Custom Messages with Custom Converters
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),
user, "birthDate", "1990-01-15");
Custom Messages in Parameterized Tests
@ParameterizedTest
@ValueSource(strings = {"alice@example.com", "bob@example.com", "carol@example.com"})
void testUserEmails(String email) {
User user = findUserByEmail(email);
assertBean(args().setMessage("User validation failed for email: {0}", email),
user, "email,isVerified", email + ",true");
}
Custom Messages with Dynamic Test Names
@TestFactory
Stream<DynamicTest> testOrders() {
return orders.stream()
.map(order -> DynamicTest.dynamicTest(
"Test Order #" + order.getId(),
() -> assertBean(
args().setMessage("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(args().setMessage(() ->
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(args().setMessage("User creation phase failed"),
user, "status", "NEW");
// Activation phase
activateUser(user);
assertBean(args().setMessage("User activation phase failed"),
user, "status", "ACTIVE");
// Verification phase
verifyUser(user);
assertBean(args().setMessage("User verification phase failed"),
user, "status,isVerified", "ACTIVE,true");
}
Batch Processing
@Test
void testBatchProcessing() {
List<Order> orders = loadOrders();
for (int i = 0; i < orders.size(); i++) {
Order order = orders.get(i);
processOrder(order);
assertBean(args().setMessage(
"Batch processing failed at index {0} of {1}, Order ID: {2}",
i, orders.size(), order.getId()),
order, "status", "PROCESSED");
}
}
Conditional Testing
@Test
void testUserPermissions() {
User user = loadUser();
String expectedRole = user.isAdmin() ? "ADMIN" : "USER";
assertBean(args().setMessage(
"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(args().setMessage(() ->
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
- Stringifiers - Converting objects to strings
- Listifiers - Converting collection-like objects to lists
- Swappers - Transforming objects before processing
- PropertyExtractors - Custom property access logic
- juneau-bct Basics - Main BCT documentation
Share feedback or follow-up questions for this page directly through GitHub.