如何基于带JSR-303约束的Java对象自动生成测试数据?
基于JSR-303注解自动生成测试数据方案
问题背景
现有带JSR-303约束的Java实体类如下:
@SuperBuilder @ToString @EqualsAndHashCode public class Address { @Size(min = 1, max = 20) @NotNull public final String line1; @Size(min = 1, max = 20) public final String line2; @Size(min = 1, max = 20) public final String city; @Size(min = 1, max = 20) public final String country; @Size(min = 1, max = 20) public final String zipCode; }
@SuperBuilder @ToString @EqualsAndHashCode public class Person { @NotNull @Valid public final Phone phone; @NotNull @Valid public final Address address; }
手动维护测试数据效率极低,需要自动生成三类测试数据:
- 最小值:可为空字段设为null,数值取类型最小值,字符串/集合取最小长度,布尔值为false
- 最大值:可为空字段非null,数值取类型最大值,字符串/集合取最大长度,布尔值为true
- 越界数据:数值低于最小值/高于最大值,字符串长度小于最小限制/大于最大限制
期望调用方式示例:
SomeMagicalGenerator.for(Person.class) .generateMaximum() // 返回Stream<Person> .distinct() .limit(10)
测试数据需支持作为TestNG DataProvider使用,尝试过Instancio但未找到适配JSR-303注解生成最值的方法,求可行方案。
可行方案
方案1:基于Instancio的自定义配置封装
Instancio支持通过自定义Model结合注解解析实现规则化数据生成,以下是封装后的工具类:
import org.instancio.Instancio; import org.instancio.Model; import org.instancio.Select; import javax.validation.constraints.*; import java.lang.reflect.Field; import java.util.stream.Stream; public class TestDataGenerator<T> { private final Class<T> targetClass; private Model<T> model; private TestDataGenerator(Class<T> targetClass) { this.targetClass = targetClass; this.model = Instancio.of(targetClass).toModel(); } public static <T> TestDataGenerator<T> forClass(Class<T> clazz) { return new TestDataGenerator<>(clazz); } public Stream<T> generateMinimum() { Model<T> minModel = model; for (Field field : targetClass.getDeclaredFields()) { // 非@NotNull字段设为null if (!field.isAnnotationPresent(NotNull.class)) { minModel = Instancio.of(minModel).set(Select.field(field), null).toModel(); } // @Size注解生成最小长度字符串 if (field.isAnnotationPresent(Size.class) && String.class.isAssignableFrom(field.getType())) { Size size = field.getAnnotation(Size.class); minModel = Instancio.of(minModel).set(Select.field(field), "x".repeat(size.min())).toModel(); } // @Min注解生成数值最小值 if (field.isAnnotationPresent(Min.class) && Number.class.isAssignableFrom(field.getType())) { Min min = field.getAnnotation(Min.class); minModel = Instancio.of(minModel).set(Select.field(field), min.value()).toModel(); } // 布尔值设为false if (boolean.class.isAssignableFrom(field.getType()) || Boolean.class.isAssignableFrom(field.getType())) { minModel = Instancio.of(minModel).set(Select.field(field), false).toModel(); } // 递归处理@Valid关联对象 if (field.isAnnotationPresent(Valid.class)) { minModel = Instancio.of(minModel) .set(Select.field(field), TestDataGenerator.forClass(field.getType()).generateMinimum().findFirst().orElse(null)) .toModel(); } } return Stream.generate(() -> Instancio.create(minModel)); } public Stream<T> generateMaximum() { Model<T> maxModel = model; for (Field field : targetClass.getDeclaredFields()) { // @Size注解生成最大长度字符串 if (field.isAnnotationPresent(Size.class) && String.class.isAssignableFrom(field.getType())) { Size size = field.getAnnotation(Size.class); maxModel = Instancio.of(maxModel).set(Select.field(field), "x".repeat(size.max())).toModel(); } // @Max注解生成数值最大值 if (field.isAnnotationPresent(Max.class) && Number.class.isAssignableFrom(field.getType())) { Max max = field.getAnnotation(Max.class); maxModel = Instancio.of(maxModel).set(Select.field(field), max.value()).toModel(); } // 布尔值设为true if (boolean.class.isAssignableFrom(field.getType()) || Boolean.class.isAssignableFrom(field.getType())) { maxModel = Instancio.of(maxModel).set(Select.field(field), true).toModel(); } // 递归处理@Valid关联对象 if (field.isAnnotationPresent(Valid.class)) { maxModel = Instancio.of(maxModel) .set(Select.field(field), TestDataGenerator.forClass(field.getType()).generateMaximum().findFirst().orElse(null)) .toModel(); } } return Stream.generate(() -> Instancio.create(maxModel)); } public Stream<T> generateOutOfBounds() { Model<T> outOfBoundsModel = model; for (Field field : targetClass.getDeclaredFields()) { // @Size注解生成越界长度字符串 if (field.isAnnotationPresent(Size.class) && String.class.isAssignableFrom(field.getType())) { Size size = field.getAnnotation(Size.class); int length = Math.random() > 0.5 ? size.min() - 1 : size.max() + 1; length = Math.max(0, length); outOfBoundsModel = Instancio.of(outOfBoundsModel).set(Select.field(field), "x".repeat(length)).toModel(); } // 数值类型生成越界值 if (field.isAnnotationPresent(Min.class) && Number.class.isAssignableFrom(field.getType())) { Min min = field.getAnnotation(Min.class); Number value = switch (field.getType().getSimpleName()) { case "int", "Integer" -> (int) min.value() - 1; case "long", "Long" -> min.value() - 1L; case "double", "Double" -> min.value() - 1.0; default -> min.value(); }; outOfBoundsModel = Instancio.of(outOfBoundsModel).set(Select.field(field), value).toModel(); } if (field.isAnnotationPresent(Max.class) && Number.class.isAssignableFrom(field.getType())) { Max max = field.getAnnotation(Max.class); Number value = switch (field.getType().getSimpleName()) { case "int", "Integer" -> (int) max.value() + 1; case "long", "Long" -> max.value() + 1L; case "double", "Double" -> max.value() + 1.0; default -> max.value(); }; outOfBoundsModel = Instancio.of(outOfBoundsModel).set(Select.field(field), value).toModel(); } // 递归处理@Valid关联对象 if (field.isAnnotationPresent(Valid.class)) { outOfBoundsModel = Instancio.of(outOfBoundsModel) .set(Select.field(field), TestDataGenerator.forClass(field.getType()).generateOutOfBounds().findFirst().orElse(null)) .toModel(); } } return Stream.generate(() -> Instancio.create(outOfBoundsModel)); } }
使用示例
// 生成最大值测试数据 Stream<Person> maxPersons = TestDataGenerator.forClass(Person.class) .generateMaximum() .distinct() .limit(10); // 作为TestNG DataProvider @DataProvider(name = "maxPersonData") public Object[][] provideMaxPersonData() { return TestDataGenerator.forClass(Person.class) .generateMaximum() .limit(5) .map(p -> new Object[]{p}) .toArray(Object[][]::new); }
方案2:自定义无依赖生成器(基于反射+Builder)
如果不想依赖第三方库,可以直接通过反射解析JSR-303注解,结合@SuperBuilder生成对象:
import javax.validation.constraints.*; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.stream.Stream; public class CustomTestDataGenerator<T> { private final Class<T> targetClass; private final Class<?> builderClass; public CustomTestDataGenerator(Class<T> targetClass) { this.targetClass = targetClass; try { this.builderClass = Class.forName(targetClass.getName() + "$Builder"); } catch (ClassNotFoundException e) { throw new RuntimeException("未找到" + targetClass.getName() + "对应的Builder类", e); } } public static <T> CustomTestDataGenerator<T> forClass(Class<T> clazz) { return new CustomTestDataGenerator<>(clazz); } public Stream<T> generateMinimum() { return Stream.generate(() -> buildInstance(true, false)); } public Stream<T> generateMaximum() { return Stream.generate(() -> buildInstance(false, true)); } public Stream<T> generateOutOfBounds() { return Stream.generate(() -> buildInstance(false, false)); } private T buildInstance(boolean isMin, boolean isMax) { try { Object builder = builderClass.getDeclaredConstructor().newInstance(); for (Field field : targetClass.getDeclaredFields()) { String setterName = "set" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1); Method setter = builderClass.getMethod(setterName, field.getType()); setter.invoke(builder, resolveFieldValue(field, isMin, isMax)); } Method buildMethod = builderClass.getMethod("build"); return targetClass.cast(buildMethod.invoke(builder)); } catch (Exception e) { throw new RuntimeException("生成" + targetClass.getName() + "实例失败", e); } } private Object resolveFieldValue(Field field, boolean isMin, boolean isMax) { Class<?> fieldType = field.getType(); // 最小值模式下非@NotNull字段设为null if (isMin && !field.isAnnotationPresent(NotNull.class)) return null; // 字符串类型处理@Size注解 if (String.class.isAssignableFrom(fieldType)) { Size size = field.getAnnotation(Size.class); if (size != null) { int length = isMin ? size.min() : (isMax ? size.max() : (Math.random() > 0.5 ? size.min() -1 : size.max() +1)); length = Math.max(0, length); return "x".repeat(length); } return isMin ? "" : "test"; } // 数值类型处理@Min/@Max注解 if (Number.class.isAssignableFrom(fieldType) || fieldType.isPrimitive()) { if (isMin) { Min min = field.getAnnotation(Min.class); return min != null ? convertNumber(min.value(), fieldType) : getPrimitiveMin(fieldType); } else if (isMax) { Max max = field.getAnnotation(Max.class); return max != null ? convertNumber(max.value(), fieldType) : getPrimitiveMax(fieldType); } else { Min min = field.getAnnotation(Min.class); Max max = field.getAnnotation(Max.class); if (min != null && Math.random() >0.5) return convertNumber(min.value()-1, fieldType); if (max != null) return convertNumber(max.value()+1, fieldType); return getPrimitiveMin(fieldType); } } // 布尔值处理 if (boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType)) { return isMin ? false : (isMax ? true : !isMin); } // 递归处理@Valid关联对象 if (field.isAnnotationPresent(Valid.class)) { CustomTestDataGenerator<?> nestedGen = CustomTestDataGenerator.forClass(fieldType); return isMin ? nestedGen.generateMinimum().findFirst().orElse(null) : isMax ? nestedGen.generateMaximum().findFirst().orElse(null) : nestedGen.generateOutOfBounds().findFirst().orElse(null); } return fieldType.isPrimitive() ? getPrimitiveDefault(fieldType) : null; } private Number convertNumber(long value, Class<?> targetType) { if (int.class.equals(targetType) || Integer.class.equals(targetType)) return (int) value; if (long.class.equals(targetType) || Long.class.equals(targetType)) return value; if (double.class.equals(targetType) || Double.class.equals(targetType)) return (double) value; if (float.class.equals(targetType) || Float.class.equals(targetType)) return (float) value; return value; } private Object getPrimitiveMin(Class<?> type) { if (int.class.equals(type)) return Integer.MIN_VALUE; if (long.class.equals(type)) return Long.MIN_VALUE; if (double.class.equals(type)) return Double.MIN_VALUE; if (float.class.equals(type)) return Float.MIN_VALUE; if (boolean.class.equals(type)) return false; return null; } private Object getPrimitiveMax(Class<?> type) { if (int.class.equals(type)) return Integer.MAX_VALUE; if (long.class.equals(type)) return Long.MAX_VALUE; if (double.class.equals(type)) return Double.MAX_VALUE; if (float.class.equals(type)) return Float.MAX_VALUE; if (boolean.class.equals(type)) return true; return null; } private Object getPrimitiveDefault(Class<?> type) { if (int.class.equals(type)) return 0; if (long.class.equals(type)) return 0L; if (double.class.equals(type)) return 0.0; if (float.class.equals(type)) return 0.0f; if (boolean.class.equals(type)) return false; return null; } }
使用示例
// 生成越界测试数据 Stream<Person> outOfBoundsPersons = CustomTestDataGenerator.forClass(Person.class) .generateOutOfBounds() .distinct() .limit(10); // TestNG DataProvider示例 @DataProvider(name = "minPersonData") public Object[][] provideMinPersonData() { return CustomTestDataGenerator.forClass(Person.class) .generateMinimum() .limit(3) .map(p -> new Object[]{p}) .toArray(Object[][]::new); }
方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| Instancio封装方案 | 无需处理复杂类型转换,支持深层嵌套对象 | 依赖第三方库,自定义规则灵活性有限 |
| 自定义无依赖生成器 | 完全可控,无外部依赖 | 需要手动处理反射、类型转换,维护成本高 |
内容的提问来源于stack exchange,提问作者Adrian Herscu
相关产品推荐
相关产品推荐

