Bean-Centric Testing Framework
A powerful and intuitive testing framework that extends JUnit with streamlined assertion methods for Java objects. BCT eliminates verbose test code while providing comprehensive object introspection and comparison capabilities.
📋 Table of Contents​
- Overview
- Quick Start
- Core Assertion Methods
- Nested Property Access
- Advanced Configuration
- Extending the Framework
- Migration Examples
Overview​
The Bean-Centric Testing Framework transforms complex multi-line JUnit assertions into simple, readable one-liners. Instead of manually extracting and comparing individual properties, BCT provides intelligent object introspection with flexible property access patterns.
Key Benefits​
- Concise Assertions: Replace many lines of manual property extraction with single assertion calls
- Powerful Property Access: Nested objects, collections, arrays, and maps with unified syntax
- Flexible Comparison: Support for custom converters, formatters, and comparison logic
- Type Safety: Comprehensive error messages with clear property paths
- Extensible: Custom property extractors, stringifiers, and conversion logic
- Minimal Dependencies: Depends only on JUnit 5 - no external libraries or heavyweight frameworks
- Zero Configuration: Works out-of-the-box with sensible defaults and automatic discovery
- 100% Test Coverage: Thoroughly tested with comprehensive unit tests ensuring reliability and stability
Real-World Impact​
BCT has been successfully deployed across the Apache Juneau project test suite with impressive results:
- Adoption Rate: 342 out of 658 test files (52%) now use BCT assertions
- Usage Statistics: Over 1,700 BCT assertion calls across the codebase
- 859
assertBean()
calls across 127 files - 663
assertList()
calls across 69 files - 192
assertBeans()
calls across 10 files
- 859
- Code Reduction: Thousands of lines of verbose property extraction replaced with concise assertions
- Test Clarity: Complex object validation simplified to single-line assertions
JUnit vs BCT Comparison​
Traditional JUnit:
// Testing a user object - verbose and repetitive
assertEquals("John", user.getName());
assertEquals(30, user.getAge());
assertTrue(user.isActive());
assertEquals("123 Main St", user.getAddress().getStreet());
assertEquals("Springfield", user.getAddress().getCity());
Bean-Centric Testing:
// Same test - concise and readable
assertBean(user, "name,age,active,address{street,city}",
"John,30,true,{123 Main St,Springfield}");
Quick Start​
1. Import Static Methods​
import static org.apache.juneau.junit.bct.BctAssertions.*;
2. Basic Object Testing​
@Test
void testUser() {
User user = new User("Alice", 25, true);
// Test multiple properties at once
assertBean(user, "name,age,active", "Alice,25,true");
// Test individual properties
assertBean(user, "name", "Alice");
assertBean(user, "age", "25");
}
@Test
void testMultipleUsers() {
List<User> users = List.of(
new User("Alice", 25, true),
new User("Bob", 30, false),
new User("Carol", 35, true)
);
// Test same properties across multiple objects
assertBeans(users, "name,age",
"Alice,25",
"Bob,30",
"Carol,35");
// Test just names
assertBeans(users, "name", "Alice", "Bob", "Carol");
}
3. Collection Testing​
@Test
void testList() {
List<String> colors = List.of("red", "green", "blue");
// Test all elements
assertList(colors, "red", "green", "blue");
// Test as bean properties
assertBean(colors, "0,1,2", "red,green,blue");
assertBean(colors, "size", "3");
}
Core Assertion Methods​
assertBean(Object, String, String)​
Tests properties of a single object using a flexible property syntax with powerful nested access capabilities.
This method provides comprehensive property access for any Java object, supporting nested objects, collections, arrays, maps, and custom field access patterns. It uses intelligent property resolution with automatic type handling and supports complex object graphs.
Basic Property Testing:​
// Simple properties
assertBean(user, "name,email", "John,john@example.com");
// Boolean properties
assertBean(user, "active,verified", "true,false");
// Null handling
assertBean(user, "middleName", "null");
Nested Object Testing:​
// Single-level nesting
assertBean(user, "address{street,city}", "{123 Main St,Springfield}");
// Multi-level nesting
assertBean(user, "profile{settings{notifications{email}}}", "{{{true}}}");
// Mixed simple and nested
assertBean(order, "id,customer{name,email},total", "12345,{John Doe,john@example.com},99.95");
Collection and Array Testing:​
// Index-based access
assertBean(order, "items{0{name},1{name}}", "{{Laptop},{Phone}}");
// Collection iteration (#{} syntax)
assertBean(order, "items{#{name}}", "[{Laptop},{Phone},{Tablet}]");
// Collection properties
assertBean(order, "items{length,#{price}}", "{3,[{999.99},{699.99},{299.99}]}");
// Array access
assertBean(data, "values{0,1,2}", "{100,200,300}");
Map Testing:​
// Direct key access
assertBean(config, "database{host,port}", "{localhost,5432}");
// Map size testing
assertBean(user, "settings{size}", "{5}");
// Null key handling
assertBean(user, "mapWithNullKey{<null>}", "{nullKeyValue}");
Field and Method Testing:​
// Public fields (no getters required)
assertBean(myBean, "f1,f2,f3", "val1,val2,val3");
// Field properties with chaining
assertBean(myBean, "f1{length},f2{class{simpleName}}", "{5},{{String}}");
// Boolean method variations
assertBean(user, "enabled,isActive,hasPermission", "true,false,true");
assertBeans(Collection, String, String...)​
Tests multiple objects in a collection, comparing the same properties across all objects using the same property access logic as assertBean
.
This method validates that each bean in a collection has the specified property values. It's perfect for testing collections of similar objects, validation results, or parsed data structures. Each expected value string corresponds to one bean in the collection.
Basic Collection Testing:​
List<User> users = Arrays.asList(
new User("Alice", 25),
new User("Bob", 30),
new User("Charlie", 35)
);
// Test same properties across all users
assertBeans(users, "name,age",
"Alice,25",
"Bob,30",
"Charlie,35"
);
Complex Nested Properties:​
// Test nested properties across multiple beans
List<Order> orders = getOrderList();
assertBeans(orders, "id,customer{name,email}",
"1,{John,john@example.com}",
"2,{Jane,jane@example.com}"
);
// Test collection properties within beans
List<ShoppingCart> carts = getCartList();
assertBeans(carts, "items{0{name}},total",
"{{Laptop}},999.99",
"{{Phone}},599.99"
);
assertList(Object, Object...)​
Tests collection elements directly with support for multiple comparison modes: string conversion, functional validation with predicates, and direct object equality.
This method supports any object that can be converted to a List, including arrays, collections, iterables, streams, and more. It provides three distinct comparison modes based on the type of expected values provided.
String Conversion Testing (Default Mode):​
// String collections - compares string representations
assertList(List.of("a", "b", "c"), "a", "b", "c");
// Number collections - converts to string for comparison
assertList(List.of(1, 2, 3), "1", "2", "3");
// Object collections - uses toString() or converter
assertList(productNames, "Laptop", "Phone", "Tablet");
// Arrays and other collection types
assertList(myArray, "element1", "element2", "element3");
Predicate Testing (Functional Validation):​
// Use Predicate<T> for functional testing
Predicate<Integer> greaterThanOne = x -> x > 1;
assertList(List.of(2, 3, 4), greaterThanOne, greaterThanOne, greaterThanOne);
// Mix predicates with other comparison types
Predicate<String> startsWithA = s -> s.startsWith("a");
assertList(List.of("apple", "banana"), startsWithA, "banana");
// Complex predicate validation
Predicate<User> isActive = user -> user.isActive();
assertList(userList, isActive, isActive);
Object Equality Testing (Direct Comparison):​
// Non-String, non-Predicate objects use Objects.equals() comparison
assertList(List.of(1, 2, 3), 1, 2, 3); // Integer objects
// Custom objects with proper equals() implementation
assertList(List.of(myBean1, myBean2), myBean1, myBean2);
// Mixed object types
assertList(mixedList, stringObj, integerObj, customObj);
assertMapped(Object, BiFunction, String, String)​
Tests objects with custom property access logic using a BiFunction, designed for objects that don't follow standard JavaBean patterns or require specialized property extraction.
This method creates an intermediate LinkedHashMap to collect all property values before using the same logic as assertBean
for comparison. This ensures consistent ordering and supports the full nested property syntax. The BiFunction receives the object and property name, returning the property value.
Custom Property Access:​
// Custom property access for non-standard objects
assertMapped(myObject, (obj, prop) -> obj.getProperty(prop),
"prop1,prop2", "value1,value2");
// Map-based property access
Map<String, Object> data = new HashMap<>();
data.put("name", "Alice");
data.put("score", 95);
assertMapped(data, (map, key) -> map.get(key),
"name,score", "Alice,95");
Exception Handling:​
// Exceptions become simple class names in output
assertMapped(dataSource, (ds, prop) -> {
try {
return ds.getConnection().getMetaData().getDatabaseProductName();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}, "databaseName", "PostgreSQL");
// Safe property access with exception conversion
assertMapped(webService, (service, endpoint) -> {
try {
return service.call(endpoint);
} catch (Exception e) {
return e.getClass().getSimpleName(); // Returns "TimeoutException"
}
}, "userEndpoint", "TimeoutException");
Complex Property Logic:​
// Transform complex nested access patterns
assertMapped(configSystem, (config, prop) -> {
switch(prop) {
case "timeout": return config.getSettings().getTimeout();
case "retries": return config.getSettings().getRetries();
case "database": return config.getDatabase().getUrl();
default: return config.getAttribute(prop);
}
}, "timeout,retries,database", "30000,3,jdbc:postgresql://localhost:5432/mydb");
// Conditional property access
assertMapped(userService, (service, operation) -> {
if ("count".equals(operation)) {
return service.getUserCount();
} else if ("active".equals(operation)) {
return service.getActiveUsers().size();
}
return service.getProperty(operation);
}, "count,active", "150,45");
Legacy System Integration:​
// Access legacy objects without standard getters
assertMapped(legacyBean, (bean, fieldName) -> {
Field field = bean.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(bean);
}, "privateField1,privateField2", "value1,value2");
Additional Assertion Methods​
BCT provides several additional assertion methods for common testing scenarios:
assertContains(String, Object)​
Tests that a string appears somewhere within the stringified object.
User user = new User("Alice Smith", 25);
List<String> items = Arrays.asList("apple", "banana", "cherry");
// Test substring presence
assertContains("Alice", user);
assertContains("Smith", user);
assertContains("banana", items);
assertContainsAll(Object, String...)​
Tests that all specified strings appear within the stringified object.
User user = new User("Alice Smith", 25);
user.setEmail("alice@example.com");
// Test multiple substrings
assertContainsAll(user, "Alice", "Smith", "25");
assertContainsAll(user, "alice", "example.com");
assertEmpty(Object)​
Tests that collections, arrays, maps, or strings are empty.
List<String> emptyList = new ArrayList<>();
String[] emptyArray = {};
Map<String,String> emptyMap = new HashMap<>();
String emptyString = "";
// Test empty collections
assertEmpty(emptyList);
assertEmpty(emptyArray);
assertEmpty(emptyMap);
assertEmpty(emptyString);
assertNotEmpty(Object)​
Tests that collections, arrays, maps, or strings are not empty.
List<String> names = Arrays.asList("Alice");
String[] colors = {"red"};
Map<String,String> config = Map.of("key", "value");
String message = "Hello";
// Test non-empty collections
assertNotEmpty(names);
assertNotEmpty(colors);
assertNotEmpty(config);
assertNotEmpty(message);
assertSize(int, Object)​
Tests the size/length of collections, arrays, maps, or strings.
List<String> names = Arrays.asList("Alice", "Bob", "Carol");
String[] colors = {"red", "green"};
Map<String,Integer> scores = Map.of("Alice", 95, "Bob", 87);
String message = "Hello";
// Test collection sizes
assertSize(3, names);
assertSize(2, colors);
assertSize(2, scores);
assertSize(5, message);
assertString(String, Object)​
Tests the string representation of an object using the configured converter.
User user = new User("Alice", 25);
List<Integer> numbers = Arrays.asList(1, 2, 3);
Date date = new Date(1609459200000L); // 2021-01-01
// Test string representations
assertString("User(name=Alice, age=25)", user);
assertString("[1, 2, 3]", numbers);
assertString("2021-01-01", date);
assertMatchesGlob(String, Object)​
Tests that the stringified object matches a glob-style pattern (* and ? wildcards).
User user = new User("Alice Smith", 25);
String filename = "report.pdf";
String email = "alice@company.com";
// Test pattern matching
assertMatchesGlob("*Alice*", user);
assertMatchesGlob("*.pdf", filename);
assertMatchesGlob("*@*.com", email);
assertMatchesGlob("User(name=Alice*, age=25)", user);
Nested Property Access​
BCT provides powerful nested property access with intuitive syntax:
Object Nesting​
// Access nested object properties
assertBean(order, "customer{address{city}}", "{{Springfield}}");
// Multiple nested levels
assertBean(user, "profile{settings{notifications{email}}}", "{{{true}}}");
// Multiple properties at each level
assertBean(order, "customer{name,email},shipping{method,cost}",
"{John,john@example.com},{Express,15.99}");
Collection and Array Access​
// Index-based access
assertBean(order, "items{0{name},1{name}}", "{{Laptop},{Phone}}");
// Iterate over all elements
assertBean(order, "items{#{name}}", "[{Laptop},{Phone},{Tablet}]");
// Collection properties
assertBean(order, "items{length,#{price}}", "{3,[{999.99},{699.99},{299.99}]}");
// Array access
assertBean(data, "values{0,1,2}", "{100,200,300}");
Map Access​
// Direct key access
assertBean(config, "database{host,port}", "{localhost,5432}");
// Special characters in keys
assertBean(props, "app.version,app.name", "1.0.0,MyApp");
// Null key access
assertBean(mapWithNullKey, "<null>", "nullKeyValue");
Size and Length Properties​
// Universal size access
assertBean(list, "size", "5");
assertBean(array, "length", "10");
assertBean(map, "size", "3");
// Combined with other properties
assertBean(user, "orders{size,#{total}}", "{3,[{99.99},{149.99},{79.99}]}");
Advanced Configuration​
Custom Error Messages​
// Static messages
assertBean(args().setMessage("User validation failed"),
user, "email", "john@example.com");
// Dynamic messages with placeholders
assertBean(args().setMessage("Test {0} failed on iteration {1}", testName, iteration),
result, "status", "SUCCESS");
// Supplier-based messages for expensive computation
assertBean(args().setMessage(() -> "Test failed at " + Instant.now()),
user, "lastLogin", expectedTime);
Custom Bean Converters​
// 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();
// Use in assertions
assertBean(args().setBeanConverter(converter),
order, "date,total", "2023-12-01,99.99");
Extending the Framework​
Custom Stringifiers​
Define how specific types should be converted to strings:
// Date formatting
Stringifier<LocalDateTime> dateStringifier = (conv, dt) ->
dt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
// Complex object formatting
Stringifier<Order> orderStringifier = (conv, order) ->
"Order#" + order.getId() + "[" + order.getStatus() + "]";
// Registration
var converter = BasicBeanConverter.builder()
.defaultSettings()
.addStringifier(LocalDateTime.class, dateStringifier)
.addStringifier(Order.class, orderStringifier)
.build();
Custom Property Extractors​
Define custom property access logic:
// Database entity extractor
PropertyExtractor entityExtractor = new PropertyExtractor() {
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return obj instanceof DatabaseEntity;
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
DatabaseEntity entity = (DatabaseEntity) obj;
switch (prop) {
case "id": return entity.getPrimaryKey();
case "displayName": return entity.computeDisplayName();
default: return entity.getAttribute(prop);
}
}
};
// Registration
var converter = BasicBeanConverter.builder()
.defaultSettings()
.addPropertyExtractor(entityExtractor)
.build();
Migration Examples​
Before and After Comparisons​
Complex Object Testing:
// Before: Traditional JUnit
@Test
void testOrderTraditional() {
assertEquals(12345L, order.getId());
assertEquals("John Doe", order.getCustomer().getName());
assertEquals("john@example.com", order.getCustomer().getEmail());
assertEquals("123 Main St", order.getShipping().getAddress().getStreet());
assertEquals("Springfield", order.getShipping().getAddress().getCity());
assertEquals(3, order.getItems().size());
assertEquals("Laptop", order.getItems().get(0).getName());
assertEquals(new BigDecimal("999.99"), order.getItems().get(0).getPrice());
assertEquals(OrderStatus.PENDING, order.getStatus());
}
// After: Bean-Centric Testing
@Test
void testOrderBCT() {
assertBean(order,
"id,customer{name,email},shipping{address{street,city}},items{size,0{name,price}},status",
"12345,{John Doe,john@example.com},{{123 Main St,Springfield}},{3,{Laptop,999.99}},PENDING");
}
Collection Testing:
// Before: Traditional JUnit
@Test
void testUsersTraditional() {
assertEquals(3, users.size());
assertEquals("Alice", users.get(0).getName());
assertEquals(25, users.get(0).getAge());
assertEquals("Bob", users.get(1).getName());
assertEquals(30, users.get(1).getAge());
assertEquals("Charlie", users.get(2).getName());
assertEquals(35, users.get(2).getAge());
}
// After: Bean-Centric Testing
@Test
void testUsersBCT() {
assertBeans(users, "name,age",
"Alice,25",
"Bob,30",
"Charlie,35");
}
Configuration Testing:
// Before: Traditional JUnit
@Test
void testConfigTraditional() {
assertEquals("localhost", config.getDatabase().getHost());
assertEquals(5432, config.getDatabase().getPort());
assertEquals("myapp", config.getDatabase().getSchema());
assertEquals(30000, config.getTimeout());
assertEquals(3, config.getRetries());
assertTrue(config.isLoggingEnabled());
}
// After: Bean-Centric Testing
@Test
void testConfigBCT() {
assertBean(config,
"database{host,port,schema},timeout,retries,loggingEnabled",
"{localhost,5432,myapp},30000,3,true");
}
Key Takeaways​
The Bean-Centric Testing Framework transforms verbose, error-prone test code into concise, readable assertions. By leveraging intelligent object introspection and flexible property access patterns, BCT enables developers to write more maintainable tests while improving test coverage and readability.
Benefits:
- Reduced Code Volume: 70-80% less test code
- Improved Readability: Clear, intention-revealing assertions
- Better Maintainability: Changes to object structure require minimal test updates
- Enhanced Error Messages: Precise failure reporting with property paths
- Flexible Extension: Custom converters for domain-specific needs
Start with simple assertBean
calls and gradually adopt more advanced features as your testing needs evolve. The framework grows with your project complexity while maintaining simplicity at its core.