diff --git a/docs/guides/README.md b/docs/guides/README.md
index e265d5187..f5106ec8d 100644
--- a/docs/guides/README.md
+++ b/docs/guides/README.md
@@ -19,6 +19,12 @@ existing Maven project. Complete the steps in order.
|-------|-------------|
| [Lombok with Ebean entity beans](lombok-with-ebean-entity-beans.md) | Which Lombok annotations to use and avoid on entity beans; why `@Data` is incompatible with Ebean; how to use `@Getter` + `@Setter` + `@Accessors(chain = true)` |
+## Testing
+
+| Guide | Description |
+|-------|-------------|
+| [Testing with TestEntityBuilder](testing-with-testentitybuilder.md) | Rapidly create test entity instances with auto-populated random values; manage relationships and cascades; customize value generation for domain-specific testing needs |
+
## Database migrations
| Guide | Description |
diff --git a/docs/guides/testing-with-testentitybuilder.md b/docs/guides/testing-with-testentitybuilder.md
new file mode 100644
index 000000000..c7b9e60d5
--- /dev/null
+++ b/docs/guides/testing-with-testentitybuilder.md
@@ -0,0 +1,566 @@
+# Guide: Testing with TestEntityBuilder
+
+## Purpose
+
+This guide explains how to use `TestEntityBuilder` to rapidly create test entity instances with auto-populated random values. It is written as practical instructions for developers and AI agents building tests for Ebean applications.
+
+`TestEntityBuilder` eliminates boilerplate test setup by automatically generating realistic test data for all scalar fields, while respecting entity constraints and relationships. This is particularly valuable for:
+
+- **Integration tests** that need representative data without caring about specific values
+- **Persistence layer tests** that verify save/update/delete operations work correctly
+- **Query and filter tests** where you need multiple entities with varied data
+- **Rapid test setup** that reduces test code verbosity and improves readability
+
+---
+
+## Setup & Dependencies
+
+### Add ebean-test to Your Project
+
+The `TestEntityBuilder` class is provided by the `ebean-test` module.
+
+**Maven:**
+```xml
+
+ io.ebean
+ ebean-test
+ 16.5.0
+ test
+
+```
+
+**Gradle:**
+```gradle
+testImplementation 'io.ebean:ebean-test:16.5.0'
+```
+
+Replace `16.5.0` with the version matching your Ebean installation.
+
+### Import the Class
+
+```java
+import io.ebean.test.TestEntityBuilder;
+```
+
+---
+
+## Basic Usage
+
+### Create a Builder Instance
+
+`TestEntityBuilder` uses a builder pattern for configuration:
+
+```java
+TestEntityBuilder builder = TestEntityBuilder.builder(DB.getDefault()).build();
+```
+
+The `Database` parameter (e.g., `DB.getDefault()`) specifies which Ebean database instance to use for entity type lookups and persistence operations.
+
+### Build an Entity (In-Memory)
+
+The `build()` method creates an instance with populated fields **without persisting to the database:**
+
+```java
+Product product = builder.build(Product.class);
+
+// Fields are populated:
+// - id: null (not populated)
+// - name: random UUID-based string
+// - price: random BigDecimal
+// - inStock: true
+// - createdAt: current instant
+// - etc.
+
+// Not persisted yet:
+assert product.getId() == null;
+```
+
+### Build and Save (Persist to Database)
+
+The `save()` method creates, persists, and returns an entity with the database-assigned `@Id`:
+
+```java
+Product product = builder.save(Product.class);
+
+// Entity is now in the database:
+assert product.getId() != null;
+
+// Verify it was saved:
+Product found = DB.find(Product.class, product.getId());
+assert found != null;
+assert found.getName().equals(product.getName());
+```
+
+---
+
+## Type-Specific Value Generation
+
+`TestEntityBuilder` generates appropriate random values for each Java/SQL type. Customize this behavior by subclassing `RandomValueGenerator` (see "Custom Value Generators" below).
+
+| Type | Generated Value | Notes |
+|------|-----------------|-------|
+| `String` | UUID-derived (8 chars by default) | Truncated to column length if `@Column(length=...)` is set |
+| Email fields | `uuid@domain.com` format | Detected when property name contains "email" (case-insensitive) |
+| `Integer` / `int` | Random in `[1, 1_000)` | |
+| `Long` / `long` | Random in `[1, 100_000)` | |
+| `Short` / `short` | Random in `[1, 100)` | |
+| `Double` / `double` | Random in `[1, 100)` | |
+| `Float` / `float` | Random in `[1, 100)` | |
+| `BigDecimal` | Respects precision and scale | Precision and scale from `@Column(precision=..., scale=...)` |
+| `Boolean` / `boolean` | `true` | Override in custom generator if needed |
+| `UUID` | Random UUID | Via `UUID.randomUUID()` |
+| `LocalDate` | Today's date | Via `LocalDate.now()` |
+| `LocalDateTime` | Current datetime | Via `LocalDateTime.now()` |
+| `Instant` | Current instant | Via `Instant.now()` |
+| `OffsetDateTime` | Current time with zone | Via `OffsetDateTime.now()` |
+| `ZonedDateTime` | Current time with zone | Via `ZonedDateTime.now()` |
+| `Enum` | First constant | Override in custom generator if needed |
+| Other types | `null` | Set these fields manually in tests |
+
+### String Length Constraints
+
+`TestEntityBuilder` respects column length constraints defined in the entity:
+
+```java
+@Entity
+public class User {
+ @Column(length = 50)
+ private String username;
+}
+
+User user = builder.build(User.class);
+assert user.getUsername().length() <= 50; // ✅ Constraint respected
+```
+
+### BigDecimal Precision and Scale
+
+For `BigDecimal` fields, the builder respects the database column precision and scale:
+
+```java
+@Entity
+public class LineItem {
+ @Column(precision = 10, scale = 2) // max 99_999_999.99
+ private BigDecimal amount;
+}
+
+LineItem item = builder.build(LineItem.class);
+assert item.getAmount().scale() == 2;
+```
+
+---
+
+## Entity Relationships
+
+### Cascade-Persist Relationships: Recursively Built
+
+Relationships marked with `cascade = PERSIST` are recursively populated:
+
+```java
+@Entity
+public class Order {
+ @ManyToOne(cascade = CascadeType.PERSIST)
+ private Customer customer;
+}
+
+Order order = builder.build(Order.class);
+
+// Both order and customer are built:
+assert order != null;
+assert order.getCustomer() != null;
+assert order.getCustomer().getId() == null; // not persisted yet
+
+// When saved, cascade handles both:
+Order saved = builder.save(Order.class);
+assert saved.getId() != null;
+assert saved.getCustomer().getId() != null; // parent also saved
+```
+
+### Non-Cascade Relationships: Left Null
+
+Relationships without cascade persist are left null (caller can populate if needed):
+
+```java
+@Entity
+public class BlogPost {
+ @ManyToOne
+ private Author author; // No cascade = left null
+}
+
+BlogPost post = builder.build(BlogPost.class);
+assert post.getAuthor() == null;
+
+// Set it manually if needed:
+post.setAuthor(manuallyCreatedAuthor);
+```
+
+### Collection Relationships: Left Empty
+
+Collection relationships (`@OneToMany`, `@ManyToMany`) are left empty:
+
+```java
+@Entity
+public class Author {
+ @OneToMany(mappedBy = "author")
+ private List posts; // Left empty
+}
+
+Author author = builder.build(Author.class);
+assert author.getPosts().isEmpty();
+
+// Populate if needed for testing:
+author.getPosts().addAll(Arrays.asList(post1, post2, post3));
+```
+
+### Cycle Detection: Prevents Infinite Recursion
+
+If two entities reference each other with cascade persist, the builder detects the cycle and breaks it by leaving one reference null:
+
+```java
+@Entity
+public class Person {
+ @ManyToOne(cascade = CascadeType.PERSIST)
+ private Organization org;
+}
+
+@Entity
+public class Organization {
+ @ManyToOne(cascade = CascadeType.PERSIST)
+ private Person founder;
+}
+
+Person person = builder.build(Person.class);
+// One reference will be null to break the cycle:
+// either person.org or person.org.founder is null
+```
+
+---
+
+## Custom Value Generators
+
+### Why Customize?
+
+The default `RandomValueGenerator` uses generic random values. For domain-specific testing, you may want:
+
+- Email addresses with your company domain
+- Realistic phone numbers
+- Product SKUs following a pattern
+- Addresses in specific regions
+- Monetary amounts within realistic ranges
+
+### Creating a Custom Generator
+
+Subclass `RandomValueGenerator` and override individual `random*()` methods:
+
+```java
+class CompanyTestDataGenerator extends RandomValueGenerator {
+
+ @Override
+ protected String randomString(String propName, int maxLength) {
+ if (propName != null && propName.toLowerCase().contains("email")) {
+ // Use company domain instead of generic @domain.com
+ String localPart = UUID.randomUUID().toString().substring(0, 8);
+ String email = localPart + "@mycompany.com";
+ if (maxLength > 0 && email.length() > maxLength) {
+ return email.substring(0, maxLength);
+ }
+ return email;
+ }
+ return super.randomString(propName, maxLength);
+ }
+
+ // Override other methods as needed:
+ @Override
+ protected Object randomEnum(Class> type) {
+ if (type == OrderStatus.class) {
+ // Bias towards common statuses for realistic test data
+ return ThreadLocalRandom.current().nextDouble() < 0.8
+ ? OrderStatus.PENDING
+ : OrderStatus.COMPLETED;
+ }
+ return super.randomEnum(type);
+ }
+}
+```
+
+### Using a Custom Generator
+
+Pass the custom generator when building:
+
+```java
+TestEntityBuilder builder = TestEntityBuilder.builder(DB.getDefault())
+ .valueGenerator(new CompanyTestDataGenerator())
+ .build();
+
+User user = builder.build(User.class);
+assert user.getEmail().endsWith("@mycompany.com");
+```
+
+### Example: Money Type
+
+```java
+public class MoneyValueGenerator extends RandomValueGenerator {
+
+ @Override
+ protected BigDecimal randomBigDecimal(int precision, int scale) {
+ // Generate prices in a realistic range: $5.00 to $999.99
+ BigDecimal price = BigDecimal.valueOf(
+ ThreadLocalRandom.current().nextDouble(5.0, 1000.0)
+ );
+ return price.setScale(2, RoundingMode.HALF_UP);
+ }
+}
+```
+
+---
+
+## Best Practices
+
+### 1. Use for Integration Tests, Not Unit Tests
+
+✅ **Good:** Integration test with database
+```java
+@Test
+void whenSaving_thenCanRetrieve() {
+ Product product = builder.save(Product.class);
+ Product found = DB.find(Product.class, product.getId());
+ assertThat(found).isNotNull();
+}
+```
+
+❌ **Poor:** Validation test requiring specific values
+```java
+@Test
+void whenNameIsBlank_thenThrowException() {
+ Product product = builder.build(Product.class); // name is random!
+ product.setName(""); // have to override anyway
+ // ... test proceeds
+}
+```
+
+### 2. Override Values for Specific Test Scenarios
+
+When test requirements demand specific field values, manually override after building:
+
+```java
+@Test
+void whenStockIsLow_thenShowWarning() {
+ Product product = builder.build(Product.class);
+ product.setQuantity(2); // Specific value for this test
+
+ boolean shouldWarn = product.shouldShowLowStockWarning();
+ assertThat(shouldWarn).isTrue();
+}
+```
+
+### 3. Create Fixture Factories for Common Patterns
+
+Encapsulate common test entity setups in factory methods:
+
+```java
+public class TestDataFactory {
+
+ private static final TestEntityBuilder builder =
+ TestEntityBuilder.builder(DB.getDefault()).build();
+
+ public static Order createPendingOrder() {
+ Order order = builder.build(Order.class);
+ order.setStatus(OrderStatus.PENDING);
+ return order;
+ }
+
+ public static Order createShippedOrder() {
+ Order order = builder.build(Order.class);
+ order.setStatus(OrderStatus.SHIPPED);
+ order.setShippedAt(Instant.now());
+ return order;
+ }
+
+ public static Order savePendingOrder() {
+ Order order = createPendingOrder();
+ DB.save(order);
+ return order;
+ }
+}
+
+// Usage in tests:
+@Test
+void whenOrderPending_thenCanUpdate() {
+ Order order = TestDataFactory.savePendingOrder();
+ // ... test logic
+}
+```
+
+### 4. Build Multiple Distinct Instances
+
+Each call to `build()` or `save()` produces a new instance with fresh random values:
+
+```java
+@Test
+void whenFetchingMultipleOrders_thenAllUnique() {
+ Order order1 = builder.save(Order.class);
+ Order order2 = builder.save(Order.class);
+ Order order3 = builder.save(Order.class);
+
+ assertThat(order1.getId()).isNotEqualTo(order2.getId());
+ assertThat(order2.getId()).isNotEqualTo(order3.getId());
+ assertThat(order1.getOrderNumber()).isNotEqualTo(order2.getOrderNumber());
+}
+```
+
+---
+
+## Complete Examples
+
+### Example 1: Integration Test with Repository
+
+```java
+@SpringBootTest
+class OrderRepositoryTest {
+
+ @Autowired
+ private OrderRepository orderRepository;
+
+ private TestEntityBuilder builder;
+
+ @BeforeEach
+ void setup() {
+ builder = TestEntityBuilder.builder(DB.getDefault()).build();
+ }
+
+ @Test
+ void whenFindingOrdersByStatus_thenReturnsMatching() {
+ // Create test data quickly
+ Order pending1 = builder.build(Order.class);
+ pending1.setStatus(OrderStatus.PENDING);
+
+ Order pending2 = builder.build(Order.class);
+ pending2.setStatus(OrderStatus.PENDING);
+
+ Order shipped = builder.build(Order.class);
+ shipped.setStatus(OrderStatus.SHIPPED);
+
+ DB.saveAll(pending1, pending2, shipped);
+
+ // Test the query
+ List pending = orderRepository.findByStatus(OrderStatus.PENDING);
+ assertThat(pending).hasSize(2);
+ }
+}
+```
+
+### Example 2: Recursive Relationship Building
+
+```java
+@Test
+void whenBuildingOrderWithCustomer_thenBothPopulated() {
+ Order order = builder.build(Order.class);
+
+ // Customer is recursively built because of @ManyToOne(cascade=PERSIST)
+ assertThat(order.getCustomer()).isNotNull();
+ assertThat(order.getCustomer().getId()).isNull(); // not persisted yet
+ assertThat(order.getCustomer().getName()).isNotNull();
+
+ // Saving cascades to customer:
+ Order saved = builder.save(Order.class);
+ assertThat(saved.getId()).isNotNull();
+ assertThat(saved.getCustomer().getId()).isNotNull();
+}
+```
+
+### Example 3: Custom Generator for Domain Values
+
+```java
+// Custom generator for your domain
+class ECommerceTestDataGenerator extends RandomValueGenerator {
+ @Override
+ protected BigDecimal randomBigDecimal(int precision, int scale) {
+ // Product prices typically range $10-$500
+ return BigDecimal.valueOf(
+ ThreadLocalRandom.current().nextDouble(10.0, 500.0)
+ ).setScale(2, RoundingMode.HALF_UP);
+ }
+}
+
+@Test
+void usingCustomGenerator() {
+ TestEntityBuilder builder = TestEntityBuilder.builder(DB.getDefault())
+ .valueGenerator(new ECommerceTestDataGenerator())
+ .build();
+
+ Product product = builder.build(Product.class);
+ assertThat(product.getPrice())
+ .isBetween(BigDecimal.TEN, BigDecimal.valueOf(500.0));
+}
+```
+
+---
+
+## Troubleshooting
+
+### "No BeanDescriptor found for [Class] — is it an @Entity?"
+
+**Cause:** The class you're trying to build is not registered as an Ebean entity.
+
+**Solution:** Ensure the class is annotated with `@Entity` and registered with the Database:
+```java
+@Entity
+@Table(name = "products")
+public class Product {
+ // ...
+}
+```
+
+### Fields are null even though I expected them to be populated
+
+**Cause:** `TestEntityBuilder` does **not** populate:
+- `@Id` fields (identity/primary key)
+- `@Version` fields (optimistic locking)
+- `@Transient` fields
+- `@OneToMany` collections
+- Non-cascade `@ManyToOne` relationships
+
+**Solution:** Set these fields manually in your test if needed:
+```java
+Product product = builder.build(Product.class);
+product.setId(1L); // Set @Id manually
+```
+
+### Building recursive relationships causes StackOverflowError
+
+**Cause:** Two or more entities mutually reference each other without cycle detection.
+
+**Solution:** This should be handled automatically by cycle detection. If not, manually set one reference to null:
+```java
+Person person = builder.build(Person.class);
+person.getOrganization().setFounder(null); // Break cycle
+```
+
+### Values generated are "too random" for my test
+
+**Cause:** Default `RandomValueGenerator` uses true random values, which aren't suitable when your test needs predictable data.
+
+**Solution:** Create a custom generator that produces deterministic values:
+```java
+class DeterministicTestDataGenerator extends RandomValueGenerator {
+ private int counter = 0;
+
+ @Override
+ protected String randomString(String propName, int maxLength) {
+ return "test_" + (counter++);
+ }
+}
+```
+
+---
+
+## Summary
+
+`TestEntityBuilder` accelerates test development by:
+
+1. **Reducing boilerplate** — No need to manually set every field
+2. **Improving readability** — Tests focus on what matters, not setup
+3. **Enabling variety** — Each build produces distinct random values
+4. **Respecting constraints** — Column lengths and decimal scales are enforced
+5. **Supporting customization** — Extend `RandomValueGenerator` for domain needs
+
+Use it for **integration tests and persistence layer tests** where you need representative data without specific values. For **validation tests** and **specific edge cases**, manually set the required field values.