Home | RU | EN

Custom Entity Generator

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.

Current Approach Problem

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.

Solution: Custom Generator

1. RandomModelGenerator Utility Class

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.

How it works in simple terms:

  1. Main task
    Imagine you have a User class with fields name, age, email. Instead of manually creating test data:
    User user = new User();
    user.setName("John");
    user.setAge(25);
    user.setEmail("john@test.com");
    This class does it automatically:
    User user = RandomModelGenerator.generateRandomModel(User.class);
    // You get an object with already filled random data
  2. How it works step by step:
    • Step 1: Object creation - Takes any class (e.g., User) and creates a new instance of that class
    • Step 2: Field analysis - Goes through all class fields (including parent class fields), ignores static fields and already filled fields
    • Step 3: Field filling - For each field there are two options:
      • Option A: Field with @RegexGen annotation - Generates a string from a regular expression and, with a certain probability, may set the field to null
      • Option B: Regular field - Generates a random value based on the field type
    • Step 4: Recursive filling - If a field is another object, it recursively fills it too
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();
    }
}

2. @RegexGen Annotation

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;
}

3. Usage in DTO Class

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;
}

4. Usage in Tests

In tests, simply write:

BookingModel model = RandomModelGenerator.generateRandomModel(BookingModel.class);

Benefits of this approach

Ready prompt for AI

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).