You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于带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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 11:27:03