@月黑风高食肉虎 噗噗虎的技术博客

魔改meanbean提高代码覆盖率

现在很多项目都用到了lombok插件。Lombok的确好用,但会有个问题,那就是很多代码(字节码)是由lombok生成的,看到不实际的代码,所以就不会有人去测,所以就会导致项目代码覆盖率很低。

我最近一个项目写完针对业务代码的全部测试,覆盖率也只有在百分之四十几,连百分之五十都不到。

其实,一般情况下,我们也不需要去测试这些代码,lombok可以保证他们的正确性。但做项目不只有开发一个人,有些项目还是很死板的,特别是有些甲方。 所以,没有办法,我们需要覆盖到(不是测试到)这些代码,但又不能花费太多精力,怎么办?

你有两个选择:1、delombok,然后自己写测试代码覆盖,但这会导致代码维护性方面的大问题,不可取。2、使用第三方工具并使用unit test来跑到这些代码来提高覆盖率。

是的,这个时候,你就需要第三方工具 meanbean 的帮助了。

meanbean 是一个POJO测试工具,项目很老,最后更新都是6年前的事情了(2012年),但出于救急,我们还是要用。 毕竟只是测试嘛,只是为了指标达标,PM汇报的时候数据好看一点,又不上生产,放心用吧。

meanbean 的使用方式我就不赘述了,请自行查阅文档。 这里要说一下 meanbean 针对 lombok 注释的 bean 使用时的一些注意事项。

首先是,meanbean 有个最大的问题,那就是它自带的 BeanTester 是无法生成不带默认构造器的 bean 的,这对我们测试一些用 lombok 的 @Value 注释的一些 bean 造成了麻烦,因为 @Value bean 默认是不会生成默认构造器的,而且针对不可变的这些 bean,大部分的时候我们会给它的字段加上 final 关键字,因此默认构造器也是 不肯能有的,这辈子都不可能有的。

最简单的解决方案就是,针对这些 bean,我们就不需要使用 BeanTester 来测试 setter / getter 了,因为他们几乎只有 getter。我们可以用 EqualsMethodTesterHashCodeMethodTester 来测试他们的 equals 和 hashCode 方法。可以写一个判断方法判断一下,然后自己实现一个非默认构造器生成 BEAN 的 FACTORY, 譬如:


     // ... in test method ...

        if (canTestBean(classOfBean)) {

            log.info("Test bean: {}", classOfBean);
            this.beanTester.testBean(classOfBean);
            this.equalsMethodTester.testEqualsMethod(classOfBean);
            this.hashCodeMethodTester.testHashCodeMethod(classOfBean);

        } else {
            if (canTestEqualsMethod(classOfBean)) {
                log.info("Test equals of bean: {}", classOfBean);
                this.equalsMethodTester.testEqualsMethod(
                    new LocalEquivalentFactory<>(classOfBean, this.beanTester));
            }

            if (canTestHashCodeMethod(classOfBean)) {
                log.info("Test hashCode of bean: {}", classOfBean);
                this.hashCodeMethodTester.testHashCodeMethod(
                    new LocalEquivalentFactory<>(classOfBean, this.beanTester));
            }
        }

    // ...

    private boolean canTestBean(Class<?> classOfBean) {
        return org.springframework.util.ClassUtils.hasConstructor(classOfBean);
    }

    private boolean canTestEqualsMethod(Class<?> classOfBean) {
        return org.springframework.util.ClassUtils.hasMethod(classOfBean, "equals", Object.class);
    }

    private boolean canTestHashCodeMethod(Class<?> classOfBean) {
        return org.springframework.util.ClassUtils.hasMethod(classOfBean, "hashCode");
    }

我们在这里自己实现了一个 LocalEquivalentFactory 来使用非默认构造器生成充满随机值的 bean 用来测试 equals 和 hashCode 方法。实现这个 factory 的方法很简单,就不多说了。

还有一个就是,我个人非常喜欢 lombok 的 @Builder,然后这块自动生成的代码又是看不到的,怎么办?这个时候我们就要魔改一下 meanbean 了。还是看代码吧:


    // in the test method
    Class<?> builderClass = findBuilder(classOfBean); // 首先通过反射找到这个 bean 类的 builder 类
    if (builderClass != null) {

        // 为这个 bean 类创建 BeanInformation
        BeanInformation beanInformation = new org.meanbean.bean.info.JavaBeanInformationFactory().create(classOfBean);
        // 为这个 bean 的 builder 类创建 BeanInformation
        BeanInformation builderInformation = new BuilderBeanInformationFactory(beanInformation).create(builderClass);
        // new 出这个 bean 的 builder 的实例
        Object newBuilder = org.springframework.util.ReflectionUtils.accessibleConstructor(builderClass).newInstance();

        // 用一个 map 存放针对每一个域生成的随机值,后面来比较各值
        Map<String, Object> testValueMap = new HashMap<>();

        // 针对每一个域(builder 里面的)
        for (PropertyInformation propertyInformation : builderInformation.getProperties()) {

            Factory<?> valueFactory;

            try {
                // 生成随机值,这个 factoryLookupStrategy 是模仿 BeanTester 里面针对每个域生产的随机值的,可以自行参照 BeanTester 里面的代码
                valueFactory = this.factoryLookupStrategy.getFactory(builderInformation, propertyInformation.getName(), propertyInformation.getWriteMethodParameterType(), null);
            } catch (NoSuchFactoryException e) {
                // 如果找不到生成随机值用的 factory,说明 beanTester 无法生成随机值
                if (!org.springframework.util.ClassUtils.hasConstructor(propertyInformation.getWriteMethodParameterType())) {
                    // 并且是由于 目标域的类型 缺少默认构造器造成的,那么我们就用自己实现的能用非默认构造器创建实例的 LocalEquivalentFactory 来生成随机值
                    valueFactory = new LocalEquivalentFactory<>(propertyInformation.getWriteMethodParameterType(), this.beanTester);
                } else {
                    // 不知道怎么处理的话,抛exception好了
                    throw e;
                }
            }
            // 找到能生成随机值的 factory 来生成目标域的测试用随机值
            Object testValue = valueFactory.create();

            // 然后写入 builder 的方法
            propertyInformation.getWriteMethod().invoke(newBuilder, testValue);

            // 保存这个随机值,后面比较用
            testValueMap.put(propertyInformation.getName(), testValue);
        }

        // build
        // 调用 XXXBuilder.build() 方法生成实例
        Method buildMethod = org.springframework.util.ClassUtils.getMethod(builderClass, "build");
        Object newBean = buildMethod.invoke(newBuilder);

        // toString,覆盖一下
        log.info("builder#toString: {}", newBuilder.toString());
        log.info("bean#toString: {}", newBean.toString());

        // check value
        for (PropertyInformation propertyInformation : beanInformation.getProperties()) {

            // 比较 由 builder 生成的值是否如预期
            Object actualValue = propertyInformation.getReadMethod().invoke(newBean);
            Object expected = testValueMap.get(propertyInformation.getName());

            assertEquals(actualValue, expected);
        }

    } else {
        // 搞不定,你看着办吧
        log.warn("bean({}) has no builder", classOfBean);
    }

    // 找 bean 里面的 builder 类
    private Class<?> findBuilder(Class<?> classOfBean) {
        // 特别要注意这个方法并不是通用的,因为 lombok 的 @Builder 可以自行指定 builder 类的名字
        // 如果不指定的话,默认就是当前类后面加个 Builder,譬如类 PlayGame 的builder就是 PlayGameBuilder
        // 但不是必然,这边根据需要自己改!
        Class<?>[] declaredClasses = classOfBean.getDeclaredClasses();
        for (Class<?> clazz : declaredClasses) {
            if (clazz.getName().endsWith("Builder")) {
                return clazz;
            }
        }
        return null;
    }

注意,我这边的代码并不是通用的,可能会有各种各样的问题。如果真有问题,别问我我也不知道。 实在搞不定的话,去跟 PM 据理力争,能不测就不测吧,大不了指标难看一点。

反正加班都这么久了,也上不了线。上线了也免不了各种问题。随你便吧。