podam mock 对象部分字段没有赋值问题

简介: 本文主要分析使用 podam mock 对象时,部分字段无法自动赋值的原因,并给出解决方案。

背景

编写单元测试的时候,经常会需要 mock 一些测试用的对象。我们采用 podam 来负责 mock 对象。按照官方文档,mock 对象非常简单:

// Simplest scenario. Will delegate to Podam all decisions
// 最简单的场景,会提供一个默认实现
PodamFactory factory = new PodamFactoryImpl();

// This will use constructor with minimum arguments and
// then setters to populate POJO
// 这个方法会使用最少参数的构造函数,然后使用 setter 进行填充
Pojo myPojo = factory.manufacturePojo(Pojo.class);

但是在使用过程中,笔者却发现生成的对象缺少了部分字段。下面是一段简单的示例代码:

   @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,   ElementType.PARAMETER, ElementType.TYPE_USE})
   @Retention(RetentionPolicy.RUNTIME)
   @Pattern(regexp = "^1[3456789]\\d{9}$", message = "手机号格式错误")
   @Constraint(validatedBy = {})
   public @interface PhoneNumber {
       String message() default "";

       Class<?>[] groups() default {};

       Class<? extends Payload>[] payload() default {};

   }


    @Data
    public static class UserDTO {
        @PhoneNumber
        private String phone;
        @Max(4)
        @Min(1)
        private Integer age;
        @Length(min = 2, max = 40)
        private String name;
        @Email
        private String email;
    }

我们需要 mock 这个类用来测试代码。根据官方文档,很容易可以写出以下代码:

    @Test
    public void testCustomAnnotation() {
        UserDTO userDTO = podamFactory.manufacturePojo(UserDTO.class);
            // 简单看一下效果
        System.out.println("userDTO : " + JSON.toJSONString(userDTO);
    }

结果控制台的输出是:

userDTO : {"age":1}

也就是说有部分字段没有注入。

当把 debug 日志打开之后,看到了更多的内容:

20:16:46.798 [main] WARN uk.co.jemos.podam.typeManufacturers.TypeManufacturerUtil - Please, register AttributeStratergy for custom constraint @com.xxx.xxx.xxx.annotation.PhoneNumber(message=, groups=[], payload=[]), in DataProviderStrategy! Value will be left to null

看来问题出现在自定义的字段校验的 @PhoneNumber 这里。

源码分析

那么为什么 @PhoneNumber 会影响字段的填充呢 ?来看源码。

首先一进来就是这个方法。很明显前两行是准备 ManufacturingContext 而已,没有实质性操作:

    @Override
    public <T> T manufacturePojo(Class<T> pojoClass, Type... genericTypeArgs) {
        // 环境准备
        ManufacturingContext manufacturingCtx = new ManufacturingContext();
        manufacturingCtx.getPojos().put(pojoClass, 1);
        // 真正的 mock 方法
        return doManufacturePojo(pojoClass, manufacturingCtx, genericTypeArgs);
    }

继续跟踪,依旧是准备工作:

    private <T> T doManufacturePojo(Class<T> pojoClass,
            ManufacturingContext manufacturingCtx, Type... genericTypeArgs) {
        try {
            Class<?> declaringClass = null;
            Object declaringInstance = null;
            AttributeMetadata pojoMetadata = new AttributeMetadata(pojoClass,
                    pojoClass, genericTypeArgs, declaringClass, declaringInstance);
          // 这里是实际的处理方法
            return this.manufacturePojoInternal(pojoClass, pojoMetadata,
                    manufacturingCtx, genericTypeArgs);
        } catch (InstantiationException e) {
            // 此处省略异常处理逻辑
        }
    }

进入 manufacturePojoInternal 方法:这里主要是根据不同的情况,采用不同的方法获取这个对象:如果从缓存获取,就直接返回,否则创建对象,并进行初始化工作。

private <T> T manufacturePojoInternal(Class<T> pojoClass,
            AttributeMetadata pojoMetadata, ManufacturingContext manufacturingCtx,
            Type... genericTypeArgs)
            throws InstantiationException, IllegalAccessException,
            InvocationTargetException, ClassNotFoundException {

        // reuse object from memoization table
        // 先从缓存中查找已经 mock 过的实例
        @SuppressWarnings("unchecked")
        T objectToReuse = (T) strategy.getMemoizedObject(pojoMetadata);
        if (objectToReuse != null) {
            LOG.debug("Fetched memoized object for {} with parameters {}",
                    pojoClass, Arrays.toString(genericTypeArgs));
            return objectToReuse;
        } else {
            LOG.debug("Manufacturing {} with parameters {}",
                    pojoClass, Arrays.toString(genericTypeArgs));
        }
        // 找不到已有的实例,继续往下走
        final Map<String, Type> typeArgsMap = new HashMap<String, Type>();
        Type[] genericTypeArgsExtra = TypeManufacturerUtil.fillTypeArgMap(typeArgsMap,
                pojoClass, genericTypeArgs);

        T retValue = (T) strategy.getTypeValue(pojoMetadata, typeArgsMap, pojoClass);
        if (null == retValue) {
            if (pojoClass.isInterface()) {

                return getValueForAbstractType(pojoClass, pojoMetadata,
                        manufacturingCtx, typeArgsMap, genericTypeArgs);
            }

            try {
                // 尝试创建这个对象
                retValue = instantiatePojo(pojoClass, manufacturingCtx, typeArgsMap,
                        genericTypeArgsExtra);
            } catch (SecurityException e) {

                throw new PodamMockeryException(
                        "Security exception while applying introspection.", e);
            }
        }

        if (retValue == null) {
            return getValueForAbstractType(pojoClass, pojoMetadata,
                    manufacturingCtx, typeArgsMap, genericTypeArgs);
        } else {

            // update memoization cache with new object
            // the reference is stored before properties are set so that recursive
            // properties can use it
            strategy.cacheMemoizedObject(pojoMetadata, retValue);

            List<Annotation> annotations = null;
            // 对象创建成功,但是还没给字段赋值,这里开始赋值
            populatePojoInternal(retValue, annotations, manufacturingCtx,
                    typeArgsMap, genericTypeArgsExtra);
        }

        return retValue;
    }

populatePojoInternal 方法中,根据不同的具体类型,调用不同的是字段赋值方法:

private <T> T populatePojoInternal(T pojo, List<Annotation> annotations,
            ManufacturingContext manufacturingCtx,
            Map<String, Type> typeArgsMap,
            Type... genericTypeArgs)
            throws InstantiationException, IllegalAccessException,
            InvocationTargetException, ClassNotFoundException {

        LOG.debug("Populating pojo {}", pojo.getClass());

      // 先判断要填充的对象的类型,对数组,Collection,Map 使用不同方法进行填充。此处省略
        Class<?> pojoClass = pojo.getClass();
        if (pojoClass.isArray()) {
            ...
        } else if (pojo instanceof Collection) {
            ...
        } else if (pojo instanceof Map) {
            ...
        }

        // 如果是 数组,Collection,或者Map类型,先填充 数组,Collection 或者 Map 的公共字段
        // 下面补充剩余的内容
        ClassInfo classInfo = classInfoStrategy.getClassInfo(pojo.getClass());

        Set<ClassAttribute> classAttributes = classInfo.getClassAttributes();
    // attribute 就是类拥有的字段,普通的 pojo 类的填充从这里开始
        for (ClassAttribute attribute : classAttributes) {
          // 填充普通的可读写字段。 ClassAttribute 中包含了字段的 getter 和 setter 方法列表。这里就是我们要找的方法了
            if (!populateReadWriteField(pojo, attribute, typeArgsMap, manufacturingCtx)) {
                populateReadOnlyField(pojo, attribute, typeArgsMap, manufacturingCtx, genericTypeArgs);
            }
        }

        // It executes any extra methods
        // 执行其他方法(应该是类似 @PostConstruct 之类的)
        Collection<Method> extraMethods = classInfoStrategy.getExtraMethods(pojoClass);
        if (null != extraMethods) {
            for (Method extraMethod : extraMethods) {

                Object[] args = getParameterValuesForMethod(extraMethod, pojoClass,
                        manufacturingCtx, typeArgsMap, genericTypeArgs);
                extraMethod.invoke(pojo, args);
            }
        }

        return pojo;
    }

populateReadWriteField 中对字段进行赋值:

    private <T> boolean populateReadWriteField(T pojo, ClassAttribute attribute,
            Map<String, Type> typeArgsMap, ManufacturingContext manufacturingCtx)
            throws InstantiationException, IllegalAccessException,
                    InvocationTargetException, ClassNotFoundException {

        Method setter = PodamUtils.selectLatestMethod(attribute.getSetters());
        if (setter == null) {
            return false;
        }

        // 此处省略对 setter 的校验代码
        ...

        // 获取字段上的注解
        List<Annotation> pojoAttributeAnnotations
                = PodamUtils.getAttributeAnnotations(
                        attribute.getAttribute(), setter);

        // 根据注解获取字段填充策略
        // 这里面是判断注解类型的方法,比较简单,就不详细分析了
        AttributeStrategy<?> attributeStrategy
                = TypeManufacturerUtil.findAttributeStrategy(strategy, pojoAttributeAnnotations, attributeType);
        Object setterArg = null;
        if (null != attributeStrategy) {

            setterArg = TypeManufacturerUtil.returnAttributeDataStrategyValue(
                    attributeType, pojoAttributeAnnotations, attributeStrategy);

        } else {
            // 此处省略没有获取到策略时的生成逻辑
            ...
        }

        // 这里调用字段的 setter,把字段填充策略生成的值赋值给字段
        try {
            setter.invoke(pojo, setterArg);
        } catch(IllegalAccessException e) {
            LOG.warn("{} is not accessible. Setting it to accessible."
                    + " However this is a security hack and your code"
                    + " should really adhere to JavaBeans standards.",
                    setter.toString());
            setter.setAccessible(true);
            setter.invoke(pojo, setterArg);
        }
        return true;
    }

打断点可以看到获取到的 AttributeStrategy<?> attributeStrategyBeanValidationStrategy ,再查看 BeanValidationStrategygetValue 方法,我们终于找到了答案:

    public Object getValue(Class<?> attrType, List<Annotation> annotations) throws PodamMockeryException {
        // 省略无关逻辑,主要是判断是否是 @Min @Max 之类的注解,并且根据注解返回对应的符合要求的值
        ...

        Pattern pattern = findTypeFromList(annotations, Pattern.class);
        if (null != pattern) {

            LOG.warn("At the moment PODAM doesn't support @Pattern({}),"
                    + " returning null", pattern.regexp());
            return null;

        }

        // 此处省略无关逻辑,主要是判断是否是 @Min @Max 之类的注解,并且根据注解返回对应的符合要求的值
        ...
        
        return null;
    }

到这里我们找到答案:加了 @Pattern 注解的字段,在 getValue 的时候,获取的是 null

总结

根据官方文档, podam 会对 @Min @Maxjavax.validation.constraints 中的部分注解进行处理,能够支持直接生成符合要求的值。但是对于 @Pattern 、或者包含 @Pattern 的其他自定义注解,以及其他注解(比如 org.hibernate.validator.constraints 中的 @Length )则无法处理,对应的注解类需要提供自定义的 AttributeStrategy。推测是因为 @Pattern 之类的比较复杂校验,比较难以生成对应的符合要求的字符串。

对于这种 @Pattern 或者自定义的校验,要编写一个对应的数值生成方法不是一个简单的事情。几经搜索,没有找到相关的案例。

最终根据官方文档提供的解决方案,需要给 podamFactory 提供一个对应的 AttributeStrategy

protected PodamFactory podamFactory = new PodamFactoryImpl();
if (podamFactory.getStrategy() instanceof RandomDataProviderStrategy) {
      // 根据实际情况提供一个随机字符串策略,这里只是简单举个例子
    AttributeStrategy<?> phoneNumberStrategy = (attrType, attrAnnotations) -> "13244443322";
    // 针对 @PhoneNumber 添加一个 AttributeStrategy
    randomDataProviderStrategy.addOrReplaceAttributeStrategy(PhoneNumber.class, phoneNumberStrategy);
    // 其他的注解也按照这种方式添加即可
}

UserDTO userDTO = factory.manufacturePojo(UserDTO.class);
相关文章
数据交互,前后端数据请求,axios请求,对象结构的使用,E6的使用,结构赋值是什么?函数形参的obj如何,函数形参的obj就改成对象结构接收传入的数据对象
数据交互,前后端数据请求,axios请求,对象结构的使用,E6的使用,结构赋值是什么?函数形参的obj如何,函数形参的obj就改成对象结构接收传入的数据对象
|
6月前
|
测试技术
反射获取或修改对象属性的值
* 获取单个对象的所有键值对
52 3
|
前端开发 Java 数据库
SpringBoot返回枚举对象中的所有属性以对象的形式返回(一个@JSONType解决)
SpringBoot返回枚举对象中的所有属性以对象的形式返回(一个@JSONType解决)
796 0
对象定义-解构-枚举属性遍历以及对象内函数
对象定义-解构-枚举属性遍历以及对象内函数
74 0
|
缓存
在 store 中定义 getter 传参
注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
101 0
lodash创建一个函数属性名称的数组,包含继承属性
lodash创建一个函数属性名称的数组,包含继承属性
86 0
lodash创建一个新的对象,对象的属性名是和传入对象一样,值则在函数中修改
lodash创建一个新的对象,对象的属性名是和传入对象一样,值则在函数中修改
119 0
lodash创建一个新的对象,对象的属性名可以修改
lodash创建一个新的对象,对象的属性名可以修改
480 0
lodash删除对象的属性,传入函数
lodash删除对象的属性,传入函数
183 0
lodash创建一个从对象中选择的属性的对象,传入函数
lodash创建一个从对象中选择的属性的对象,传入函数
75 0
下一篇
无影云桌面