魔改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。我们可以用
EqualsMethodTester
和 HashCodeMethodTester
来测试他们的 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 据理力争,能不测就不测吧,大不了指标难看一点。
反正加班都这么久了,也上不了线。上线了也免不了各种问题。随你便吧。
- 完 -