In tests, we often create entities like users and set their field values either manually or using data generation. However, we always have to do this by manually writing the logic, as we cannot create an entity with already generated fields.
For example, this is how it looks in a test:
User user = User.builder()
.username(RandomData.getUsername())
.password(RandomData.getPassword())
.role(UserRole.USER.toString())
.build();
Here (above), by the way, we use a custom RandomData class:
public class RandomData {
private RandomData() {
}
public static String getUsername() {
return RandomStringUtils.randomAlphabetic(4, 15);
}
public static String getPassword() {
return RandomStringUtils.randomAlphabetic(6).toLowerCase() +
RandomStringUtils.randomAlphabetic(3).toUpperCase() +
RandomStringUtils.randomNumeric(3) + "!";
}
}
But this is not the best approach. Ideally, we need to create an entity with already generated field values in one line of code. This is exactly why we create our custom entity generator. Generally, it's more convenient to do this through AI now, by passing context and setting the prompt correctly.
First, you need to create a RandomModelGenerator utility class with corresponding static methods. Mine looks like this, don't spend too much time understanding it, it's needed as an example for AI.
This class is a random data generator for testing. It automatically creates objects of any classes and fills their fields with random values.
Important: The class uses a third-party library Generex for generating strings from regular expressions. Alternatively, you can use the library RgxGen. The main thing is to specify this in the prompt for AI when generating code.
User user = new User();
user.setName("John");
user.setAge(25);
user.setEmail("john@test.com");
User user = RandomModelGenerator.generateRandomModel(User.class);
// You get an object with already filled random data
public final class RandomModelGenerator {
private static final ThreadLocalRandom RND = ThreadLocalRandom.current();
private RandomModelGenerator() {}
public static <T> T generateRandomModel(Class<T> clazz) {
try {
T instance = newInstance(clazz);
fillObject(instance);
return instance;
} catch (Exception e) {
throw new RuntimeException("Failed to generate model for " + clazz.getName(), e);
}
}
// --- internals ---
private static void fillObject(Object obj) throws Exception {
if (obj == null) return;
for (Field f : getAllFields(obj.getClass())) {
if (Modifier.isStatic(f.getModifiers())) continue;
f.setAccessible(true);
// don't touch already set values (if someone set them via builder beforehand)
if (f.get(obj) != null) continue;
Class<?> type = f.getType();
RegexGen rg = f.getAnnotation(RegexGen.class);
if (rg != null) {
// null probability
if (rg.nullProbability() > 0 && RND.nextDouble() < rg.nullProbability()) {
f.set(obj, null);
continue;
}
String generated = new Generex(rg.value()).random();
Object coerced = coerceToType(generated, type);
f.set(obj, coerced);
continue;
}
// without annotation - set default random/empty values or recursively fill nested model
f.set(obj, defaultValueFor(type));
}
}
private static Object coerceToType(String s, Class<?> type) throws Exception {
if (type == String.class) return s;
if (type == Integer.class || type == int.class) return Integer.parseInt(s);
if (type == Long.class || type == long.class) return Long.parseLong(s);
if (type == Double.class || type == double.class) return Double.parseDouble(s);
if (type == Boolean.class || type == boolean.class) return Boolean.parseBoolean(s);
if (type == LocalDate.class) return LocalDate.parse(s); // expect ISO format yyyy-MM-dd
if (type == LocalDateTime.class) return LocalDateTime.parse(s); // expect ISO format yyyy-MM-ddTHH:mm:ss
if (type.isEnum()) {
@SuppressWarnings({"rawtypes", "unchecked"})
Enum value = Enum.valueOf((Class<Enum>) type, s);
return value;
}
// Nested object: if string can't be converted - try to recursively create object
Object nested = tryCreateAndFillNested(type);
if (nested != null) return nested;
// fallback - leave string if field is String, otherwise null
return type == String.class ? s : null;
}
private static Object defaultValueFor(Class<?> type) throws Exception {
if (type == String.class) return randomAlphaNum(8);
if (type == Integer.class || type == int.class) return RND.nextInt(0, 10_000);
if (type == Long.class || type == long.class) return Math.abs(RND.nextLong());
if (type == Double.class || type == double.class) return RND.nextDouble();
if (type == Boolean.class || type == boolean.class) return RND.nextBoolean();
if (type == LocalDate.class) return LocalDate.now().plusDays(RND.nextInt(0, 30));
if (type == LocalDateTime.class) return LocalDateTime.now().plusMinutes(RND.nextInt(0, 60));
if (type.isEnum()) {
Object[] constants = type.getEnumConstants();
return constants.length == 0 ? null : constants[RND.nextInt(constants.length)];
}
// Collections and arrays - simply: empty
if (Collection.class.isAssignableFrom(type)) return Collections.emptyList();
if (type.isArray()) return Array.newInstance(type.getComponentType(), 0);
// Consider it a nested model
return tryCreateAndFillNested(type);
}
private static Object tryCreateAndFillNested(Class<?> type) throws Exception {
try {
Object nested = newInstance(type);
fillObject(nested);
return nested;
} catch (Exception ignored) {
return null;
}
}
private static <T> T newInstance(Class<T> type) throws Exception {
Constructor<T> ctor = type.getDeclaredConstructor();
ctor.setAccessible(true);
return ctor.newInstance();
}
private static List<Field> getAllFields(Class<?> type) {
List<Field> fields = new ArrayList<>();
for (Class<?> c = type; c != null && c != Object.class; c = c.getSuperclass()) {
fields.addAll(Arrays.asList(c.getDeclaredFields()));
}
return fields;
}
private static String randomAlphaNum(int len) {
final String alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) sb.append(alphabet.charAt(RND.nextInt(alphabet.length())));
return sb.toString();
}
}
We need to create a custom annotation that we will attach to DTO class fields to specify the logic for generating field values:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface RegexGen {
/** Regular expression for generating field value */
String value();
/** Probability to make field null (0.0..1.0). By default not null. */
double nullProbability() default 0.0;
}
In the DTO class, place this annotation and regexp above each field that needs to be generated:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BookingModel extends BaseModel{
@RegexGen("[A-Z][a-z]{3,10}")
public String firstname;
@RegexGen("[A-Z][a-z]{3,12}")
public String lastname;
@RegexGen("\\d{2,4}")
public Integer totalprice;
@RegexGen("true|false")
public Boolean depositpaid;
public BookingdatesModel bookingdates;
@RegexGen("Breakfast|Dinner|Late checkout")
public String additionalneeds;
}
In tests, simply write:
BookingModel model = RandomModelGenerator.generateRandomModel(BookingModel.class);
Create a custom entity generator RandomModelGenerator with a static
method generateRandomModel(), which takes a DTO class as a parameter and
creates an object of this class while immediately generating values for
its fields based on the @RegexGen annotation applied to the field of
this class with a specified regexp, for example:
@RegexGen("[A-Z][a-z]{3,10}")
public String firstname;
If there is no annotation, do not generate any value for the field. So
that I can write
User user =
RandomModelGenerator.generateRandomModel(User.class)
and an object of the User class with generated fields will be created.
You can use the third-party library com.github.curious-odd-man/rgxgen.
Also add the ability to randomly choose values from existing Enums in
the project. And if a nested class is used as a field in the DTO class,
then values should be generated for it as well.
This prompt generates everything needed + usage examples. Moreover,
later you can always extend the functionality of this class by asking to
add the ability to generate dates and times in a specific format, and so
on.
Ideally, you should use AI with the context of your project (like
Cursor).