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.
- π Project Site - Complete project documentation with examples and guides
- π§ API Documentation - Comprehensive Javadoc reference with examples
- π Test Reports - JUnit test execution results and coverage
- π¦ Dependencies - Project dependency analysis
- π Project Reports - Complete Maven site reports
Note: All documentation is automatically updated with each commit to provide the latest project information.
- Overview
- Quick Start
- Core Assertion Methods
- Nested Property Access
- Advanced Configuration
- Extending the Framework
- Migration Examples
- Javadoc Syntax Highlighting
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.
- π― 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
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
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}");import static org.bct.api.BctAssertions.*;@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");
}@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");
}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.
// Simple properties
assertBean(user, "name,email", "John,[email protected]");
// Boolean properties
assertBean(user, "active,verified", "true,false");
// Null handling
assertBean(user, "middleName", "null");// 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,[email protected]},99.95");// 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}");// 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}");// 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");- Simple values:
"value"for direct property values - Nested values:
"{value}"for single-level nested properties - Deep nested:
"{{value}}","{{{value}}}"for multiple nesting levels - Collections:
"[item1,item2]"for collection values - Collection iteration:
"#{property}"iterates over ALL elements - Universal size:
"length"and"size"work on arrays, collections, maps - Boolean values:
"true","false" - Null values:
"null"
- Collection/Array access: Numeric indices (e.g.,
"0","1") - Universal size properties:
"length"and"size" - Map key access: Direct key lookup (including
"<null>") - Boolean methods:
is{Property}()methods - Getter methods:
get{Property}()methods - Public fields: Direct field access
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.
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"
);// Test nested properties across multiple beans
List<Order> orders = getOrderList();
assertBeans(orders, "id,customer{name,email}",
"1,{John,[email protected]}",
"2,{Jane,[email protected]}"
);
// Test collection properties within beans
List<ShoppingCart> carts = getCartList();
assertBeans(carts, "items{0{name}},total",
"{{Laptop}},999.99",
"{{Phone}},599.99"
);// Test validation results
List<ValidationError> errors = validator.validate(form);
assertBeans(errors, "field,message,code",
"email,Invalid email format,E001",
"age,Must be 18 or older,E002"
);// Test collection iteration within beans (#{...} syntax)
List<Department> departments = getDepartmentList();
assertBeans(departments, "name,employees{#{name}}",
"Engineering,[{Alice},{Bob},{Charlie}]",
"Marketing,[{David},{Eve}]"
);// Test parsed object collections
var parsed = JsonParser.DEFAULT.parse(jsonArray, MyBean[].class);
assertBeans(Arrays.asList(parsed), "prop1,prop2",
"val1,val2",
"val3,val4"
);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 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");// 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);// 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);- Collections: List, Set, Queue, Deque, etc.
- Arrays: Both primitive and object arrays
- Iterables: Any object implementing Iterable
- Iterators: Iterator instances
- Streams: Stream objects
- Enumerations: Legacy Enumeration objects
- Optional: Single-value Optional objects
- Maps: Converted to list of entries
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 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");// 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");// 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");// 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");BCT provides several additional assertion methods for common testing scenarios:
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);Tests that all specified strings appear within the stringified object.
User user = new User("Alice Smith", 25);
user.setEmail("[email protected]");
// Test multiple substrings
assertContainsAll(user, "Alice", "Smith", "25");
assertContainsAll(user, "alice", "example.com");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);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);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);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);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 = "[email protected]";
// Test pattern matching
assertMatchesGlob("*Alice*", user);
assertMatchesGlob("*.pdf", filename);
assertMatchesGlob("*@*.com", email);
assertMatchesGlob("User(name=Alice*, age=25)", user);BCT provides powerful nested property access with intuitive syntax:
// 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,[email protected]},{Express,15.99}");// 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}");// 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");// 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}]}");// Static messages
assertBean(args().setMessage("User validation failed"),
user, "email", "[email protected]");
// 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);// 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");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();Define how collection-like objects should be converted to lists:
// Custom collection handling
Listifier<ResultSet> resultSetListifier = (conv, rs) -> {
var results = new ArrayList<>();
while (rs.next()) {
results.add(rs.getRowData());
}
return results;
};
// Stream processing
Listifier<Stream> streamListifier = (conv, stream) ->
stream.collect(toList());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();Define object pre-processing logic:
// Optional unwrapping (already built-in)
Swapper<Optional> optionalSwapper = (conv, opt) -> opt.orElse(null);
// Custom wrapper unwrapping
Swapper<LazyValue> lazySwapper = (conv, lazy) ->
lazy.isEvaluated() ? lazy.getValue() : "<unevaluated>";
// Future handling
Swapper<CompletableFuture> futureSwapper = (conv, future) -> {
try {
return future.isDone() ? future.get() : "<pending>";
} catch (Exception e) {
return "<error: " + e.getMessage() + ">";
}
};Complex Object Testing:
// Before: Traditional JUnit
@Test
void testOrderTraditional() {
assertEquals(12345L, order.getId());
assertEquals("John Doe", order.getCustomer().getName());
assertEquals("[email protected]", 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,[email protected]},{{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");
}This framework uses custom HTML tags in Javadocs for enhanced syntax highlighting:
<jk>tags: Java keywords (class,var,return, etc.)<jv>tags: Local variables and field names<jp>tags: Parameters in lambda expressions and methods<jsm>tags: Static method calls<jsf>tags: Static fields and constants<js>tags: String literals<jc>tags: Comments
The stylesheet for these tags provides professional syntax highlighting in generated documentation, making code examples more readable and visually appealing.
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.