diff --git a/README.md b/README.md
index 9c01d7567e..5dda8a0ae0 100644
--- a/README.md
+++ b/README.md
@@ -84,6 +84,13 @@ Goto [https://ebean.io/docs/](https://ebean.io/docs/)
## Guides
Step-by-step guides for common tasks: [docs/guides/](docs/guides/README.md)
+Available guides:
+- [Maven POM setup](docs/guides/add-ebean-postgres-maven-pom.md)
+- [Database configuration](docs/guides/add-ebean-postgres-database-config.md)
+- [Test container setup](docs/guides/add-ebean-postgres-test-container.md)
+- [DB migration generation](docs/guides/add-ebean-db-migration-generation.md)
+- [Lombok with Ebean entity beans](docs/guides/lombok-with-ebean-entity-beans.md)
+
## Maven central
[Maven central - g:io.ebean](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22io.ebean%22%20)
diff --git a/ebean-test/src/main/java/io/ebean/test/RandomValueGenerator.java b/ebean-test/src/main/java/io/ebean/test/RandomValueGenerator.java
new file mode 100644
index 0000000000..b121e88c27
--- /dev/null
+++ b/ebean-test/src/main/java/io/ebean/test/RandomValueGenerator.java
@@ -0,0 +1,202 @@
+package io.ebean.test;
+
+import io.ebeaninternal.server.deploy.BeanProperty;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * Generates random values for entity bean properties in tests.
+ *
+ * The primary entry point is {@link #generate(BeanProperty)}, which is
+ * property-aware (e.g. caps String values at the column's max length).
+ * The secondary entry point {@link #generate(Class)} works on the Java type
+ * alone and is useful when no property metadata is available.
+ *
+ *
+ * All per-type factory methods ({@link #randomString(String, int)},
+ * {@link #randomBigDecimal(int, int)}, {@link #randomLong()}, etc.) are
+ * {@code protected} so that subclasses can override individual types without
+ * replacing the full dispatch logic.
+ *
+ *
+ * Returns {@code null} for types that are not mapped (exotic / unknown types) —
+ * the caller is expected to set those fields manually.
+ *
+ */
+public class RandomValueGenerator {
+
+ /**
+ * Generate a random value for the given bean property.
+ *
+ * For {@code String} properties, the value is capped at the column's
+ * {@link BeanProperty#dbLength()} when that length is positive.
+ * For {@code BigDecimal} properties, precision and scale from the column
+ * definition are used.
+ *
+ */
+ public Object generate(BeanProperty prop) {
+ Class> type = prop.type();
+ if (type == String.class) {
+ return randomString(prop.name(), prop.dbLength());
+ }
+ if (type == BigDecimal.class) {
+ return randomBigDecimal(prop.dbLength(), prop.dbScale());
+ }
+ return generate(type);
+ }
+
+ /**
+ * Generate a random value for the given Java type, without property metadata.
+ *
+ * String values produced here use a fixed 8-character length. Use
+ * {@link #generate(BeanProperty)} when column-length constraints matter.
+ *
+ */
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public Object generate(Class> type) {
+ if (type == null) return null;
+ if (type == String.class) return randomString(null, 0);
+ if (type == Boolean.class || type == boolean.class) return randomBoolean();
+ if (type == UUID.class) return randomUUID();
+ if (type == Instant.class) return randomInstant();
+ if (type == OffsetDateTime.class) return randomOffsetDateTime();
+ if (type == ZonedDateTime.class) return randomZonedDateTime();
+ if (type == LocalDate.class) return randomLocalDate();
+ if (type == LocalDateTime.class) return randomLocalDateTime();
+ if (type == Long.class || type == long.class) return randomLong();
+ if (type == Integer.class || type == int.class) return randomInt();
+ if (type == Short.class || type == short.class) return randomShort();
+ if (type == BigDecimal.class) return randomBigDecimal(0, -1);
+ if (type == Double.class || type == double.class) return randomDouble();
+ if (type == Float.class || type == float.class) return randomFloat();
+ if (type.isEnum()) return randomEnum(type);
+ return null; // exotic/unknown type — caller must set this field manually
+ }
+
+ /** Return a random {@code long} in [1, 100_000). */
+ protected long randomLong() {
+ return ThreadLocalRandom.current().nextLong(1, 100_000L);
+ }
+
+ /** Return a random {@code int} in [1, 1_000). */
+ protected int randomInt() {
+ return ThreadLocalRandom.current().nextInt(1, 1_000);
+ }
+
+ /** Return a random {@code short} in [1, 100). */
+ protected short randomShort() {
+ return (short) ThreadLocalRandom.current().nextInt(1, 100);
+ }
+
+ /** Return a random {@code boolean} — defaults to {@code true}. */
+ protected boolean randomBoolean() {
+ return true;
+ }
+
+ /** Return a random {@link UUID}. */
+ protected UUID randomUUID() {
+ return UUID.randomUUID();
+ }
+
+ /** Return a random {@link Instant} — defaults to now. */
+ protected Instant randomInstant() {
+ return Instant.now();
+ }
+
+ /** Return a random {@link OffsetDateTime} — defaults to now. */
+ protected OffsetDateTime randomOffsetDateTime() {
+ return OffsetDateTime.now();
+ }
+
+ /** Return a random {@link ZonedDateTime} — defaults to now. */
+ protected ZonedDateTime randomZonedDateTime() {
+ return ZonedDateTime.now();
+ }
+
+ /** Return a random {@link LocalDate} — defaults to today. */
+ protected LocalDate randomLocalDate() {
+ return LocalDate.now();
+ }
+
+ /** Return a random {@link LocalDateTime} — defaults to now. */
+ protected LocalDateTime randomLocalDateTime() {
+ return LocalDateTime.now();
+ }
+
+ /** Return a random {@code double} in [1, 100). */
+ protected double randomDouble() {
+ return ThreadLocalRandom.current().nextDouble(1, 100);
+ }
+
+ /** Return a random {@code float} in [1, 100). */
+ protected float randomFloat() {
+ return (float) ThreadLocalRandom.current().nextDouble(1, 100);
+ }
+
+ /**
+ * Return a value for the given enum type — defaults to the first declared constant.
+ * Returns {@code null} if the enum has no constants.
+ */
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ protected Object randomEnum(Class> type) {
+ Object[] constants = type.getEnumConstants();
+ return constants.length > 0 ? constants[0] : null;
+ }
+
+ /**
+ * Generate a random string, optionally capped at {@code maxLength}.
+ *
+ * When the property name contains "email" (case-insensitive), a value of the
+ * form {@code @domain.com} is returned, truncated to fit {@code maxLength}.
+ * Otherwise a UUID-derived string is returned, truncated to {@code maxLength}
+ * (defaulting to 8 characters when {@code maxLength} is 0 or negative).
+ *
+ */
+ protected String randomString(String propName, int maxLength) {
+ String base = UUID.randomUUID().toString().replace("-", ""); // 32 chars
+ if (propName != null && propName.toLowerCase().contains("email")) {
+ String email = base.substring(0, 8) + "@domain.com";
+ if (maxLength > 0 && email.length() > maxLength) {
+ int localLen = maxLength - "@domain.com".length();
+ email = (localLen > 0 ? base.substring(0, localLen) : base.substring(0, 1)) + "@domain.com";
+ }
+ return email;
+ }
+ if (maxLength <= 0) {
+ return base.substring(0, 8);
+ }
+ int len = Math.min(maxLength, base.length());
+ return base.substring(0, len);
+ }
+
+ /**
+ * Generate a random {@link BigDecimal} that fits within the given precision and scale.
+ *
+ * {@code precision} is the total number of significant digits ({@code dbLength});
+ * {@code scale} is the number of decimal places ({@code dbScale}).
+ * When precision is 0 or negative (unknown), a default of 6 integer digits is used.
+ * When scale is negative (unknown), a default scale of 2 is used.
+ *
+ */
+ protected BigDecimal randomBigDecimal(int precision, int scale) {
+ int actualScale = scale >= 0 ? scale : 2;
+ int intDigits = precision > 0 ? Math.max(1, precision - actualScale) : 6;
+ long maxInt = (long) Math.pow(10, intDigits) - 1;
+ long intPart = ThreadLocalRandom.current().nextLong(1, maxInt + 1);
+ if (actualScale == 0) {
+ return BigDecimal.valueOf(intPart);
+ }
+ long scaleFactor = (long) Math.pow(10, actualScale);
+ long fracPart = ThreadLocalRandom.current().nextLong(0, scaleFactor);
+ double value = intPart + (double) fracPart / scaleFactor;
+ return BigDecimal.valueOf(value).setScale(actualScale, RoundingMode.HALF_UP);
+ }
+}
diff --git a/ebean-test/src/main/java/io/ebean/test/TestEntityBuilder.java b/ebean-test/src/main/java/io/ebean/test/TestEntityBuilder.java
new file mode 100644
index 0000000000..4483c777a0
--- /dev/null
+++ b/ebean-test/src/main/java/io/ebean/test/TestEntityBuilder.java
@@ -0,0 +1,153 @@
+package io.ebean.test;
+
+import io.ebean.Database;
+import io.ebean.bean.EntityBean;
+import io.ebeaninternal.server.deploy.BeanDescriptor;
+import io.ebeaninternal.server.deploy.BeanProperty;
+import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
+import io.ebeaninternal.server.deploy.BeanPropertyAssocOne;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Builds entity bean instances with randomly populated scalar fields for use in tests.
+ *
+ * {@code @Id} and {@code @Version} properties are left at their defaults (zero/null).
+ * {@code @ManyToOne} and {@code @OneToOne} relationships that have cascade persist are
+ * recursively built and set. Collection relationships ({@code @OneToMany},
+ * {@code @ManyToMany}) are left empty — the caller can populate them if needed.
+ *
+ * {@code
+ * TestEntityBuilder builder = TestEntityBuilder.builder(DB.getDefault()).build();
+ *
+ * // build in-memory (not saved to database)
+ * MyEntity entity = builder.build(MyEntity.class);
+ *
+ * // build and insert to the database
+ * MyEntity entity = builder.save(MyEntity.class);
+ *
+ * // supply a custom value generator for domain-specific values
+ * TestEntityBuilder builder = TestEntityBuilder.builder(DB.getDefault())
+ * .valueGenerator(myGenerator)
+ * .build();
+ * }
+ */
+public class TestEntityBuilder {
+
+ private final Database database;
+ private final RandomValueGenerator valueGenerator;
+
+ private TestEntityBuilder(Database database, RandomValueGenerator valueGenerator) {
+ this.database = database;
+ this.valueGenerator = valueGenerator;
+ }
+
+ /** Returns a new {@link Builder} for the given database. */
+ public static Builder builder(Database database) {
+ return new Builder(database);
+ }
+
+ /** Builder for {@link TestEntityBuilder}. */
+ public static final class Builder {
+
+ private final Database database;
+ private RandomValueGenerator valueGenerator;
+
+ private Builder(Database database) {
+ this.database = requireNonNull(database);
+ }
+
+ /** Override the default {@link RandomValueGenerator}, e.g. for domain-specific value generation. */
+ public Builder valueGenerator(RandomValueGenerator valueGenerator) {
+ this.valueGenerator = valueGenerator;
+ return this;
+ }
+
+ /** Build and return a {@link TestEntityBuilder}. */
+ public TestEntityBuilder build() {
+ if (valueGenerator == null) {
+ valueGenerator = new RandomValueGenerator();
+ }
+ return new TestEntityBuilder(database, valueGenerator);
+ }
+ }
+
+ /**
+ * Build and return an instance of the entity class with scalar fields populated
+ * with random values. The entity is not saved to the database.
+ *
+ * @param beanClass the entity class to build
+ * @throws IllegalArgumentException if the class is not a known Ebean entity
+ */
+ public T build(Class beanClass) {
+ return build(beanClass, new HashSet<>());
+ }
+
+ /**
+ * Build an instance of the entity class, insert it to the database, and return it.
+ *
+ * @param beanClass the entity class to build and save
+ * @throws IllegalArgumentException if the class is not a known Ebean entity
+ */
+ public T save(Class beanClass) {
+ T bean = build(beanClass);
+ database.save(bean);
+ return bean;
+ }
+
+ private T build(Class beanClass, Set> buildStack) {
+ BeanDescriptor descriptor = (BeanDescriptor) database.pluginApi().beanType(beanClass);
+ if (descriptor == null) {
+ throw new IllegalArgumentException("No BeanDescriptor found for " + beanClass.getName()
+ + " — is it an @Entity registered with this Database?");
+ }
+
+ Set importedSaveNames = importedSavePropertyNames(descriptor);
+
+ T bean = descriptor.createBean();
+ buildStack.add(beanClass);
+ try {
+ for (BeanProperty prop : descriptor.propertiesAll()) {
+ if (prop.isId() || prop.isVersion() || prop.isGenerated() || prop.isTransient()) {
+ continue;
+ }
+ if (prop instanceof BeanPropertyAssocMany) {
+ // leave collections empty — caller populates if needed
+ continue;
+ }
+ if (prop instanceof BeanPropertyAssocOne) {
+ if (importedSaveNames.contains(prop.name())) {
+ BeanPropertyAssocOne> assocOne = (BeanPropertyAssocOne>) prop;
+ Class> targetType = assocOne.targetType();
+ if (!buildStack.contains(targetType)) {
+ Object related = build(targetType, buildStack);
+ prop.setValue((EntityBean) bean, related);
+ }
+ // else: cycle detected — leave the reference null
+ }
+ // non-cascade-save association — leave null
+ continue;
+ }
+ // scalar property
+ Object value = valueGenerator.generate(prop);
+ if (value != null) {
+ prop.setValue((EntityBean) bean, value);
+ }
+ }
+ } finally {
+ buildStack.remove(beanClass);
+ }
+ return bean;
+ }
+
+ private Set importedSavePropertyNames(BeanDescriptor> descriptor) {
+ Set names = new HashSet<>();
+ for (BeanPropertyAssocOne> p : descriptor.propertiesOneImportedSave()) {
+ names.add(p.name());
+ }
+ return names;
+ }
+}
diff --git a/ebean-test/src/test/java/io/ebean/test/RandomValueGeneratorTest.java b/ebean-test/src/test/java/io/ebean/test/RandomValueGeneratorTest.java
new file mode 100644
index 0000000000..a17cd459b3
--- /dev/null
+++ b/ebean-test/src/test/java/io/ebean/test/RandomValueGeneratorTest.java
@@ -0,0 +1,242 @@
+package io.ebean.test;
+
+import io.ebean.DB;
+import io.ebean.xtest.BaseTestCase;
+import io.ebeaninternal.server.deploy.BeanDescriptor;
+import io.ebeaninternal.server.deploy.BeanProperty;
+import org.junit.jupiter.api.Test;
+import org.tests.cache.personinfo.PersonOther;
+import org.tests.model.basic.EBasic;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class RandomValueGeneratorTest extends BaseTestCase {
+
+ private final RandomValueGenerator generator = new RandomValueGenerator();
+
+ private BeanDescriptor descriptor(Class cls) {
+ return (BeanDescriptor) DB.getDefault().pluginApi().beanType(cls);
+ }
+
+ @Test
+ void generate_stringType_returnsEightCharString() {
+ Object value = generator.generate(String.class);
+
+ assertThat(value).isInstanceOf(String.class);
+ assertThat((String) value).hasSize(8);
+ }
+
+ @Test
+ void generate_stringPropWithLength_cappedAtDbLength() {
+ BeanDescriptor descriptor = descriptor(EBasic.class);
+ BeanProperty nameProp = descriptor.findProperty("name");
+
+ assertThat(nameProp.dbLength()).isEqualTo(127);
+ Object value = generator.generate(nameProp);
+
+ assertThat(value).isInstanceOf(String.class);
+ assertThat((String) value).hasSizeLessThanOrEqualTo(127);
+ assertThat((String) value).isNotEmpty();
+ }
+
+ @Test
+ void generate_stringPropWithNoLength_returnsEightCharString() {
+ BeanDescriptor descriptor = descriptor(EBasic.class);
+ BeanProperty descProp = descriptor.findProperty("description");
+
+ // description has no @Size annotation — dbLength is 0 (unlimited)
+ Object value = generator.generate(descProp);
+
+ assertThat(value).isInstanceOf(String.class);
+ assertThat((String) value).hasSize(8);
+ }
+
+ @Test
+ void generate_variousScalarTypes_returnsExpectedTypes() {
+ assertThat(generator.generate(Long.class)).isInstanceOf(Long.class);
+ assertThat(generator.generate(long.class)).isInstanceOf(Long.class);
+ assertThat(generator.generate(Integer.class)).isInstanceOf(Integer.class);
+ assertThat(generator.generate(int.class)).isInstanceOf(Integer.class);
+ assertThat(generator.generate(Short.class)).isInstanceOf(Short.class);
+ assertThat(generator.generate(short.class)).isInstanceOf(Short.class);
+ assertThat(generator.generate(Boolean.class)).isEqualTo(Boolean.TRUE);
+ assertThat(generator.generate(boolean.class)).isEqualTo(Boolean.TRUE);
+ assertThat(generator.generate(UUID.class)).isInstanceOf(UUID.class);
+ assertThat(generator.generate(Instant.class)).isInstanceOf(Instant.class);
+ assertThat(generator.generate(OffsetDateTime.class)).isInstanceOf(OffsetDateTime.class);
+ assertThat(generator.generate(ZonedDateTime.class)).isInstanceOf(ZonedDateTime.class);
+ assertThat(generator.generate(LocalDate.class)).isInstanceOf(LocalDate.class);
+ assertThat(generator.generate(LocalDateTime.class)).isInstanceOf(LocalDateTime.class);
+ assertThat(generator.generate(BigDecimal.class)).isInstanceOf(BigDecimal.class);
+ assertThat(generator.generate(Double.class)).isInstanceOf(Double.class);
+ assertThat(generator.generate(Float.class)).isInstanceOf(Float.class);
+ }
+
+ @Test
+ void generate_enumType_returnsFirstConstant() {
+ Object value = generator.generate(EBasic.Status.class);
+ assertThat(value).isEqualTo(EBasic.Status.NEW);
+ }
+
+ @Test
+ void generate_unknownType_returnsNull() {
+ assertThat(generator.generate(Object.class)).isNull();
+ assertThat(generator.generate((Class>) null)).isNull();
+ }
+
+ @Test
+ void generate_emailPropName_returnsEmailAddress() {
+ BeanDescriptor descriptor = descriptor(PersonOther.class);
+ BeanProperty emailProp = descriptor.findProperty("email");
+
+ assertThat(emailProp.dbLength()).isEqualTo(60);
+ Object value = generator.generate(emailProp);
+
+ assertThat(value).isInstanceOf(String.class);
+ assertThat((String) value).contains("@domain.com");
+ assertThat((String) value).hasSizeLessThanOrEqualTo(60);
+ }
+
+ @Test
+ void generate_bigDecimalType_returnsScaledValue() {
+ Object value = generator.generate(BigDecimal.class);
+
+ assertThat(value).isInstanceOf(BigDecimal.class);
+ BigDecimal decimal = (BigDecimal) value;
+ assertThat(decimal.scale()).isEqualTo(2);
+ assertThat(decimal).isGreaterThan(BigDecimal.ZERO);
+ }
+
+
+ @Test
+ void generate_stringPropWithLengthShorterThan32_truncatesCorrectly() {
+ BeanDescriptor descriptor = descriptor(EBasic.class);
+ BeanProperty nameProp = descriptor.findProperty("name");
+
+ // Run many times to verify no value ever exceeds the limit
+ for (int i = 0; i < 20; i++) {
+ Object value = generator.generate(nameProp);
+ assertThat((String) value).hasSizeLessThanOrEqualTo(127);
+ }
+ }
+
+ // --- direct protected-method tests (no database needed) ---
+
+ @Test
+ void randomLong_isPositiveAndInRange() {
+ long v = generator.randomLong();
+ assertThat(v).isBetween(1L, 100_000L);
+ }
+
+ @Test
+ void randomInt_isPositiveAndInRange() {
+ int v = generator.randomInt();
+ assertThat(v).isBetween(1, 999);
+ }
+
+ @Test
+ void randomShort_isPositiveAndInRange() {
+ short v = generator.randomShort();
+ assertThat((int) v).isBetween(1, 99);
+ }
+
+ @Test
+ void randomBoolean_returnsTrue() {
+ assertThat(generator.randomBoolean()).isTrue();
+ }
+
+ @Test
+ void randomUUID_returnsValidUUID() {
+ UUID v = generator.randomUUID();
+ assertThat(v).isNotNull();
+ assertThat(v.toString()).hasSize(36);
+ }
+
+ @Test
+ void randomInstant_returnsInstant() {
+ assertThat(generator.randomInstant()).isInstanceOf(Instant.class);
+ }
+
+ @Test
+ void randomOffsetDateTime_returnsOffsetDateTime() {
+ assertThat(generator.randomOffsetDateTime()).isInstanceOf(OffsetDateTime.class);
+ }
+
+ @Test
+ void randomZonedDateTime_returnsZonedDateTime() {
+ assertThat(generator.randomZonedDateTime()).isInstanceOf(ZonedDateTime.class);
+ }
+
+ @Test
+ void randomLocalDate_returnsLocalDate() {
+ assertThat(generator.randomLocalDate()).isInstanceOf(LocalDate.class);
+ }
+
+ @Test
+ void randomLocalDateTime_returnsLocalDateTime() {
+ assertThat(generator.randomLocalDateTime()).isInstanceOf(LocalDateTime.class);
+ }
+
+ @Test
+ void randomDouble_isPositiveAndInRange() {
+ double v = generator.randomDouble();
+ assertThat(v).isBetween(1.0, 100.0);
+ }
+
+ @Test
+ void randomFloat_isPositiveAndInRange() {
+ float v = generator.randomFloat();
+ assertThat((double) v).isBetween(1.0, 100.0);
+ }
+
+ @Test
+ void randomBigDecimal_defaultPrecisionAndScale_scaleIsTwo() {
+ BigDecimal v = generator.randomBigDecimal(0, -1);
+ assertThat(v.scale()).isEqualTo(2);
+ assertThat(v).isGreaterThan(BigDecimal.ZERO);
+ }
+
+ @Test
+ void randomBigDecimal_explicitPrecisionAndScale_fitsWithinBounds() {
+ // DECIMAL(8,3) → max integer part 99999, scale 3
+ BigDecimal v = generator.randomBigDecimal(8, 3);
+ assertThat(v.scale()).isEqualTo(3);
+ assertThat(v.precision()).isLessThanOrEqualTo(8);
+ assertThat(v).isGreaterThan(BigDecimal.ZERO);
+ }
+
+ @Test
+ void randomString_emailPropName_containsDomainSuffix() {
+ String v = generator.randomString("emailAddress", 0);
+ assertThat(v).endsWith("@domain.com");
+ }
+
+ @Test
+ void randomString_regularPropName_returnsEightChars() {
+ String v = generator.randomString("name", 0);
+ assertThat(v).hasSize(8);
+ }
+
+ @Test
+ void randomString_withMaxLength_isTruncated() {
+ String v = generator.randomString("description", 5);
+ assertThat(v).hasSize(5);
+ }
+
+ @Test
+ void subclassCanOverrideRandomLong() {
+ RandomValueGenerator fixed = new RandomValueGenerator() {
+ @Override
+ protected long randomLong() { return 42L; }
+ };
+ assertThat(fixed.generate(Long.class)).isEqualTo(42L);
+ }
+}
diff --git a/ebean-test/src/test/java/io/ebean/test/TestEntityBuilderTest.java b/ebean-test/src/test/java/io/ebean/test/TestEntityBuilderTest.java
new file mode 100644
index 0000000000..47a0bf9ba4
--- /dev/null
+++ b/ebean-test/src/test/java/io/ebean/test/TestEntityBuilderTest.java
@@ -0,0 +1,86 @@
+package io.ebean.test;
+
+import io.ebean.DB;
+import io.ebean.xtest.BaseTestCase;
+import org.junit.jupiter.api.Test;
+import org.tests.model.basic.EBasic;
+import org.tests.model.basic.UUOne;
+import org.tests.model.basic.UUTwo;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class TestEntityBuilderTest extends BaseTestCase {
+
+ private final TestEntityBuilder builder = TestEntityBuilder.builder(DB.getDefault()).build();
+
+ @Test
+ void build_simpleEntity_populatesScalarFields() {
+ EBasic bean = builder.build(EBasic.class);
+
+ assertThat(bean).isNotNull();
+ assertThat(bean.getId()).isNull(); // @Id — not populated
+ assertThat(bean.getName()).isNotNull(); // String scalar — populated
+ assertThat(bean.getName()).hasSizeLessThanOrEqualTo(127); // @Size(max=127) respected
+ assertThat(bean.getDescription()).isNotNull();
+ assertThat(bean.getStatus()).isNotNull(); // Enum — populated with first constant
+ assertThat(bean.getStatus()).isEqualTo(EBasic.Status.NEW);
+ }
+
+ @Test
+ void build_entityWithCascadeManyToOne_populatesRelationship() {
+ UUTwo bean = builder.build(UUTwo.class);
+
+ assertThat(bean).isNotNull();
+ assertThat(bean.getId()).isNull(); // @Id — not populated
+ assertThat(bean.getVersion()).isZero(); // @Version — not populated
+ assertThat(bean.getName()).isNotNull();
+ assertThat(bean.getMaster()).isNotNull(); // @ManyToOne(cascade=PERSIST) — recursively built
+ assertThat(bean.getMaster().getName()).isNotNull();
+ assertThat(bean.getMaster().getId()).isNull(); // @Id on UUOne — not populated
+ assertThat(bean.getMaster().getVersion()).isZero(); // @Version on UUOne — not populated
+ }
+
+ @Test
+ void build_calledTwice_producesDistinctInstances() {
+ EBasic first = builder.build(EBasic.class);
+ EBasic second = builder.build(EBasic.class);
+
+ assertThat(first).isNotSameAs(second);
+ // String values should be different random values
+ assertThat(first.getName()).isNotEqualTo(second.getName());
+ }
+
+ @Test
+ void save_insertsEntityAndReturnsWithId() {
+ EBasic saved = builder.save(EBasic.class);
+
+ assertThat(saved).isNotNull();
+ assertThat(saved.getId()).isNotNull(); // @Id assigned after insert
+ assertThat(saved.getName()).isNotNull();
+
+ // verify it's actually in the database
+ EBasic found = DB.find(EBasic.class, saved.getId());
+ assertThat(found).isNotNull();
+ assertThat(found.getName()).isEqualTo(saved.getName());
+ }
+
+ @Test
+ void save_entityWithCascadeManyToOne_savesCascades() {
+ UUTwo saved = builder.save(UUTwo.class);
+
+ assertThat(saved).isNotNull();
+ assertThat(saved.getId()).isNotNull();
+ assertThat(saved.getMaster()).isNotNull();
+ assertThat(saved.getMaster().getId()).isNotNull(); // parent also saved via cascade
+
+ UUOne foundMaster = DB.find(UUOne.class, saved.getMaster().getId());
+ assertThat(foundMaster).isNotNull();
+ }
+
+ @Test
+ void build_unknownClass_throwsIllegalArgumentException() {
+ org.assertj.core.api.Assertions.assertThatThrownBy(() -> builder.build(String.class))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("No BeanDescriptor found");
+ }
+}
diff --git a/ebean-test/src/test/java/org/tests/cache/personinfo/PersonOther.java b/ebean-test/src/test/java/org/tests/cache/personinfo/PersonOther.java
index 6ec0569c8f..1d15779a81 100644
--- a/ebean-test/src/test/java/org/tests/cache/personinfo/PersonOther.java
+++ b/ebean-test/src/test/java/org/tests/cache/personinfo/PersonOther.java
@@ -3,6 +3,7 @@
import io.ebean.annotation.WhenCreated;
import io.ebean.annotation.WhenModified;
+import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
@@ -16,6 +17,7 @@ public class PersonOther {
@Size(max=128)
private String id;
+ @Column(length = 60)
private String email;
@WhenCreated