Swappers
Swappers transform objects before they are processed by BCT assertions. They provide a way to unwrap, evaluate, or transform objects into more testable forms. Swappers are particularly useful for wrapper types like Optional, Supplier, and Future.
Built-in Swappers
BCT comes with built-in swappers for common wrapper and lazy-evaluation types:
// Optional types
Optional<String> optional = Optional.of("Hello");
assertString("Hello", optional); // Uses optionalSwapper() - unwraps to "Hello"
Optional<String> emptyOptional = Optional.empty();
assertString("<null>", emptyOptional); // Uses optionalSwapper() - unwraps to null
// Supplier types
Supplier<String> supplier = () -> "World";
assertString("World", supplier); // Uses supplierSwapper() - calls get() to get "World"
Supplier<String> expensiveSupplier = () -> {
// Simulate expensive computation
return "Expensive Result";
};
assertString("Expensive Result", expensiveSupplier); // Uses supplierSwapper() - calls get()
// Future types
CompletableFuture<String> completedFuture = CompletableFuture.completedFuture("Done");
assertString("Done", completedFuture); // Uses futureSwapper() - gets completed result
CompletableFuture<String> pendingFuture = new CompletableFuture<>();
assertString("<pending>", pendingFuture); // Uses futureSwapper() - shows pending status
Available Built-in Swappers
optionalSwapper()- Unwraps Optional values to their contained value or nullsupplierSwapper()- Calls Supplier.get() to evaluate lazy valuesfutureSwapper()- Extracts completed Future results or shows "<pending>" for incomplete futures
Custom Swappers
Define custom swappers for your domain-specific wrapper types:
Basic Custom Swapper
// Lazy wrapper swapper
Swapper<LazyValue> lazySwapper = (conv, lazy) -> {
if (lazy == null) return null;
return lazy.getValue(); // Evaluate the lazy value
};
// Result wrapper swapper
Swapper<Result> resultSwapper = (conv, result) -> {
if (result == null) return null;
if (result.isSuccess()) {
return result.getValue();
} else {
return "Error: " + result.getError();
}
};
// Registration
var converter = BasicBeanConverter.builder()
.defaultSettings()
.addSwapper(LazyValue.class, lazySwapper)
.addSwapper(Result.class, resultSwapper)
.build();
Advanced Swapper Examples
// Either/Union type swapper
Swapper<Either<String, Integer>> eitherSwapper = (conv, either) -> {
if (either == null) return null;
return either.isLeft() ? either.getLeft() : either.getRight();
};
// Validation result swapper
Swapper<ValidationResult> validationSwapper = (conv, validation) -> {
if (validation == null) return null;
if (validation.isValid()) {
return validation.getValue();
} else {
// Return list of error messages
return validation.getErrors();
}
};
// Proxy object swapper
Swapper<ProxyObject> proxySwapper = (conv, proxy) -> {
if (proxy == null) return null;
// Unwrap the proxy to get the real object
return proxy.getTarget();
};
// Registration
var converter = BasicBeanConverter.builder()
.defaultSettings()
.addSwapper(Either.class, eitherSwapper)
.addSwapper(ValidationResult.class, validationSwapper)
.addSwapper(ProxyObject.class, proxySwapper)
.build();
Conditional Swapper
// Swap based on object state
Swapper<CachedValue> cachedValueSwapper = (conv, cached) -> {
if (cached == null) return null;
if (cached.isExpired()) {
return "<expired>";
} else if (cached.isPending()) {
return "<pending>";
} else {
return cached.getValue();
}
};
// Usage
assertString("myValue", cachedValue); // "myValue" if valid
assertString("<expired>", expiredValue); // "<expired>" if expired
Chained Swapper
// Swapper that performs multiple transformations
Swapper<ComplexWrapper> complexSwapper = (conv, wrapper) -> {
if (wrapper == null) return null;
// Step 1: Extract inner value
Object inner = wrapper.getInner();
// Step 2: Apply transformation
if (inner instanceof Optional) {
inner = ((Optional<?>) inner).orElse(null);
}
// Step 3: Apply conversion
if (inner instanceof Supplier) {
inner = ((Supplier<?>) inner).get();
}
return inner;
};
Usage Examples
Testing Wrapped Values
// Test Optional values
Optional<User> optUser = userService.findById(123);
assertBean(optUser, "name,email", "John,john@example.com");
// Test Supplier values
Supplier<Config> configSupplier = () -> loadConfig();
assertBean(configSupplier, "timeout,retries", "30000,3");
// Test Future values
CompletableFuture<Order> futureOrder = orderService.getOrderAsync(456);
futureOrder.join(); // Wait for completion
assertBean(futureOrder, "id,total", "456,99.99");
Testing Result Types
// Test Result wrapper with custom swapper
Result<User> result = userService.createUser(userData);
assertBean(args().setBeanConverter(converter),
result, "name,email", "Alice,alice@example.com");
// 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");
Testing Lazy Values
// Test lazy computation
LazyValue<Report> lazyReport = new LazyValue<>(() -> generateReport());
assertBean(args().setBeanConverter(converter),
lazyReport, "title,itemCount", "Monthly Report,150");
// Swapper ensures the lazy value is evaluated before testing
Important Considerations
When to Use Swappers vs. Stringifiers
Use Swappers when:
- You need to unwrap or transform objects before property extraction
- The object is a wrapper type (Optional, Supplier, Result, etc.)
- You want to change the object structure for testing
- Property access needs to work on the unwrapped value
Use Stringifiers when:
- You only need to change the string representation
- The object structure is fine, just needs better formatting
- You want to customize how the object appears in assertions
Swapper Execution Order
Swappers are applied before any other processing:
// 1. Swapper unwraps the Optional
Optional<User> optUser = Optional.of(new User("Alice", 25));
// 2. PropertyExtractor accesses properties on the unwrapped User
// 3. Stringifier formats the property values
assertBean(optUser, "name,age", "Alice,25");
Thread Safety
// Ensure thread-safe swappers for concurrent testing
Swapper<ThreadSafeLazy> threadSafeSwapper = (conv, lazy) -> {
if (lazy == null) return null;
synchronized (lazy) {
return lazy.getValue();
}
};
Error Handling
// Robust swapper with error handling
Swapper<AsyncValue> safeAsyncSwapper = (conv, async) -> {
if (async == null) return null;
try {
return async.get(1, TimeUnit.SECONDS);
} catch (TimeoutException e) {
return "<timeout>";
} catch (Exception e) {
return "<error: " + e.getMessage() + ">";
}
};
Best Practices
When to Create Custom Swappers
- Domain-specific wrapper types
- Lazy evaluation containers
- Result/Either/Try types
- Async/Future-like types
- Proxy objects
- Validation wrappers
Swapper Guidelines
- Always handle null input appropriately
- Return null for null input (maintain null semantics)
- Keep swappers simple and focused on unwrapping/transformation
- Avoid expensive operations in swappers when possible
- Document any side effects (e.g., evaluating lazy values)
- Consider thread safety for shared swappers
- Handle exceptions gracefully with meaningful error messages
Performance Considerations
- Swappers are called for every object being tested
- Avoid expensive operations (database calls, network requests)
- Consider caching for expensive transformations
- Be careful with lazy evaluation - ensure it's what you want
- Use timeouts for async operations
Naming Conventions
// Use descriptive names that indicate the transformation
optionalSwapper() // Unwraps Optional
supplierSwapper() // Evaluates Supplier
futureSwapper() // Extracts Future result
resultSwapper() // Unwraps Result type
eitherSwapper() // Extracts Either value
validationSwapper() // Unwraps ValidationResult
Common Patterns
Monadic Types
// Optional/Maybe
Swapper<Optional> optionalSwapper = (conv, opt) ->
opt != null ? opt.orElse(null) : null;
// Try/Result
Swapper<Try> trySwapper = (conv, tryValue) ->
tryValue != null ? (tryValue.isSuccess() ? tryValue.get() : tryValue.getError()) : null;
// Either
Swapper<Either> eitherSwapper = (conv, either) ->
either != null ? (either.isLeft() ? either.getLeft() : either.getRight()) : null;
Async/Future Types
// CompletableFuture
Swapper<CompletableFuture> futureSwapper = (conv, future) -> {
if (future == null) return null;
if (future.isDone()) {
try {
return future.get();
} catch (Exception e) {
return "<error>";
}
}
return "<pending>";
};
Validation Types
// Validation result with errors
Swapper<Validation> validationSwapper = (conv, validation) -> {
if (validation == null) return null;
return validation.isValid() ? validation.getValue() : validation.getErrors();
};
See Also
- Stringifiers - Converting objects to strings
- Listifiers - Converting collection-like objects to lists
- PropertyExtractors - Custom property access logic
- juneau-bct Basics - Main BCT documentation
Share feedback or follow-up questions for this page directly through GitHub.