В тестах мы частенько создавая какую-то сущность, например юзера, задаем значения его полей или вручную или с помощью генерации данных. Однако нам всегда надо это делать, прописывая руками логику, так как мы не можем создать сущность с уже сгенерированными полями.
Например, так это выглядит в тесте:
User user = User.builder()
.username(RandomData.getUsername())
.password(RandomData.getPassword())
.role(UserRole.USER.toString())
.build();
Тут (выше), кстати, мы используем самописный класс RandomData:
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) + "!";
}
}
Но это не лучший подход. В идеале нужно одной строчкой кода создавать сущность с уже сгенерированными значениями для полей. Для этого как раз мы и создаем свой кастомный генератор сущностей. Вообще сейчас удобнее делать это через AI, передав контекст и правильно задав промпт.
Сначала надо создать утилитарный класс RandomModelGenerator с соответствующими static методами. Мой например выглядит так, не стоит долго в нем разбираться, он нужен в качестве примера для AI.
Этот класс - генератор случайных данных для тестирования. Он автоматически создает объекты любых классов и заполняет их поля случайными значениями.
Важно: Класс использует стороннюю библиотеку Generex для генерации строк по регулярным выражениям. Альтернативно можно использовать библиотеку RgxGen. Главное - прописать это в промпте для AI при генерации кода.
User user = new User();
user.setName("Иван");
user.setAge(25);
user.setEmail("ivan@test.com");
User user = RandomModelGenerator.generateRandomModel(User.class);
// Получаете объект с уже заполненными случайными данными
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);
}
}
// --- внутренности ---
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);
// не трогаем уже установленные значения (если кто-то builder-ом подставил заранее)
if (f.get(obj) != null) continue;
Class<?> type = f.getType();
RegexGen rg = f.getAnnotation(RegexGen.class);
if (rg != null) {
// вероятность null
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;
}
// без аннотации - ставим дефолтные случайные/пустые значения или рекурсивно заполняем вложенную модель
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); // ожидаем ISO-формат yyyy-MM-dd
if (type == LocalDateTime.class) return LocalDateTime.parse(s); // ожидаем ISO-формат yyyy-MM-ddTHH:mm:ss
if (type.isEnum()) {
@SuppressWarnings({"rawtypes", "unchecked"})
Enum value = Enum.valueOf((Class<Enum>) type, s);
return value;
}
// Вложенный объект: если строку нельзя привести - пробуем рекурсивно создать объект
Object nested = tryCreateAndFillNested(type);
if (nested != null) return nested;
// fallback - оставить строку, если поле String, иначе 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)];
}
// Коллекции и массивы - по-простому: пустые
if (Collection.class.isAssignableFrom(type)) return Collections.emptyList();
if (type.isArray()) return Array.newInstance(type.getComponentType(), 0);
// Считаем, что это вложенная модель
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();
}
}
Нужно создать кастомную аннотацию, которую будем навешивать на поля DTO класса, чтобы указать логику генерации значений для полей:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface RegexGen {
/** Регулярное выражение для генерации значения поля */
String value();
/** Вероятность сделать поле null (0.0..1.0). По умолчанию не null. */
double nullProbability() default 0.0;
}
В DTO классе проставить данную аннотацию и regexp над каждым полем, которое нужно генерировать:
@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;
}
В тестах просто пишем:
BookingModel model = RandomModelGenerator.generateRandomModel(BookingModel.class);
Создай мне кастомный генератор сущностей RandomModelGenerator со
статическим методом generateRandomModel(), который в качестве параметра
принимает DTO класс и создает объект этого класса сразу генерируя
значения для его полей на основе аннотации @RegexGen, навешенной на поле
этого класса с указанием regexp, например так:
@RegexGen("[A-Z][a-z]{3,10}")
public String firstname;
Если аннотации нет, то не генерируй никакого значения для поля. Чтобы я
мог написать
User user = RandomModelGenerator.generateRandomModel(User.class)
и создался объект класса User с сгенерированными полями. Можешь
использовать стороннюю библиотеку
com.github.curious-odd-man/rgxgen.
Также добавь возможность чтобы можно было рандомно выбирать значения из
существующих в проекте Enum. И чтобы если в качестве поля в DTO классе
используется другой вложенный класс, то для него так же генерировались
значения.
Данный промпт генерирует все что надо + примеры использования. Кроме того, потом вы всегда можете расширить функциональность данного класса, попросив добавить возможность генерации дат и времени в определенном формате и так далее. В идеале надо использовать AI с контекстом вашего проекта (типа Cursor).