Skip to main content

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("&lt;null&gt;", 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("&lt;pending&gt;", pendingFuture); // Uses futureSwapper() - shows pending status

Available Built-in Swappers

  • optionalSwapper() - Unwraps Optional values to their contained value or null
  • supplierSwapper() - Calls Supplier.get() to evaluate lazy values
  • futureSwapper() - 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 "&lt;expired&gt;";
} else if (cached.isPending()) {
return "&lt;pending&gt;";
} else {
return cached.getValue();
}
};

// Usage
assertString("myValue", cachedValue); // "myValue" if valid
assertString("&lt;expired&gt;", expiredValue); // "&lt;expired&gt;" 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 "&lt;timeout&gt;";
} catch (Exception e) {
return "&lt;error: " + e.getMessage() + "&gt;";
}
};

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 "&lt;error&gt;";
}
}
return "&lt;pending&gt;";
};

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

Discussion

Share feedback or follow-up questions for this page directly through GitHub.