测试@ NonNull / @ Nullable批注

而不是“专用于...”


下文描述的任务不是创新性的,也不是有用的,我工作的公司不会因此获利,但我会得到好处。

但是这项任务已经完成,因此必须解决。

前言


在本文中,您经常会遇到“龙目岛(Lombok)”一词,我请那些讨厌的人不要急于下结论。
我不会因为龙目岛(Lombok)或它的缺席而“沉迷”,我像Geralt Sapkovsky一样保持中立,并且我可以在有或没有龙目岛的情况下冷静地阅读代码,而不会在本世纪颤抖。

但是在当前项目中,存在提到的库,并且有些东西告诉我,我们的项目不是唯一的一个。
所以在这里。

Java上一次出现肯定是朝annotashki的趋势。 快速失败概念的荣耀在于,方法的参数通常使用@NonNull批注进行批注(这样,如果出现问题,则可以解决)。

对此有很多导入选项(或意识形态上的注释类似),但是,由于已经很清楚,我们将重点关注版本

import lombok.NonNull; 

如果您使用此(或类似的)注释,那么您需要与一些合同进行测试检查,任何静态代码分析器都会告诉您(Sonar会告诉您)。

使用单元测试来测试此批注非常简单,问题在于这样的测试将以春季兔子的速度在您的项目中成倍增加,并且您知道兔子违反了DRY原理。

在本文中,我们将编写一个小的测试框架来测试@NonNull批注的约定(这样,Sonar不会在您的眼睛中发出讨厌的红光)。

附言:这首歌的名字是受PowerWolf乐队的歌启发的,当我写下名字时,这首歌(由golly演奏)(名字听起来很肯定)

主体


最初,我们测试了注释,如下所示:

 @Test void methodNameWithNullArgumentThrowException() { try { instance.getAnyType(null); fail("Exception not thrown"); } catch (final NullPointerException e) { assertNotNull(e); } } 

他们调用了该方法并传递了null作为带有@NonNull批注的参数。
他们获得了NPE并感到满意(Sonar也很高兴)。

然后他们开始做同样的事情,但是有了一个更时尚的assertThrow,它可以通过Supplier(我们喜欢lambda)工作:

 @TestUnitRepeatOnce void methodNameWithNullArgumentThrowException() { assertThrows(NullPointerException.class, () -> instance.getAnyType(null)); } 

时尚。 时尚。 青年时期

似乎可以完成,注释已经过测试,还有什么呢?

有一天,当我为某个方法编写测试,该测试方法的问题(不是问题,但仍然是“弹出”)成功完成后,我注意到该参数上没有@NonNull批注。

这是可以理解的:您可以通过when()/ then()调用test方法,而不描述Moque类的行为。 执行线程安全地在未锁定(或锁定,但没有when()/ then())对象上的某个地方捕获NPE的情况下进入该方法,但是如您所警告的那样,崩溃时使用了NPE,这意味着测试是绿色的

事实证明,在这种情况下,我们正在测试,而不是注释,但是不清楚是什么。 在测试正常进行的情况下,我们甚至不必更深入地研究该方法(降低阈值)。
Lombok的@NonNull批注具有一个功能:如果我们从NPE变为批注,则将参数名称写入错误。

我们将参与其中,从NPE脱离后,我们还将另外检查stacktrace的文本,如下所示:

 exception.getCause().getMessage().equals(parameter.getName()) 

如果突然...
如果Lombok突然刷新并停止编写在堆栈跟踪中接收到null的参数的名称,那么我们将回顾Andrei Pangin在JVM TI上的演讲,并为JVM编写一个插件,在其中我们将传递参数名称。

一切似乎一无是处,现在我们真的检查了需要什么,但是“兔子”问题没有解决。

我想要一个可以说的工具,例如:

 @TestUnitRepeatOnce @SneakyThrows void nonNullAnnotationTest() { assertNonNullAnnotation(YourPerfectClass.class); } 

他本人将去扫描指定类的所有公共方法,并通过测试检查其所有@NonNull参数。

您会说,进行反思,并检查方法@NonNull是否打开以及其中是否有项目符号为null。

一切都将一事无成,但RetentionPolicy并非如此。

所有注释都具有RetentionPolicy参数,该参数可以为3种类型:SOURCE,CLASS和RUNTIME,因此Lombok默认具有RetentionPolicy.SOURCE,这意味着该注释在Runtime中不可见,并且您不会通过反射找到它。

在我们的项目中,对公共方法的所有参数都进行了注释(不计算基元),如果可以理解参数不能为null,则假定参数相反,则该参数由spring @Nullable进行注释。 您可以参与其中,我们将寻找所有公共方法,以及其中所有未标记为@Nullable且不是原语的参数。
我们的意思是,对于所有其他情况,注释@NonNull应该在参数上。

为了方便起见,我们将尽可能地通过私有方法来传播逻辑,首先,我们将获得所有公共方法:

 private List<Method> getPublicMethods(final Class clazz) { return Arrays.stream(clazz.getDeclaredMethods()) .filter(METHOD_FILTER) .collect(toList()); } 

其中METHOD_FILTER是一个常规谓词,其中我们说:

  • 该方法必须是公开的
  • 它不应该是句法的(当您使用带有原始参数的方法时,就会发生这种情况)
  • 它不应该是抽象的(关于单独的抽象类和下面的抽象类)
  • 方法名不应该等于(如果某种邪恶的人决定在我们的POJO框架的输入中决定使用覆盖的equals()填充类)

在获得所需的所有方法之后,我们开始循环地对它们进行排序,
如果该方法根本没有任何参数,则这不是我们的候选方法:

 if (method.getParameterCount() == 0) { continue; } 

如果有参数,我们需要了解它们是否带有@NonNull注释(更确切地说,根据

逻辑学
  • 公共方法
  • 不是@Nullable
  • 不是原始的


为此,制作一个地图,并根据方法中的顺序将参数放入其中,并在它们对面放置一个标志,以指示@NonNull注释是否应位于上方:

 int nonNullAnnotationCount = 0; int index = 0; val parameterCurrentMethodArray = method.getParameters(); val notNullAnnotationParameterMap = new HashMap<Integer, Boolean>(); for (val parameter : parameterCurrentMethodArray) { if (isNull(parameter.getAnnotation(Nullable.class)) && isFalse(parameter.getType().isPrimitive())) { notNullAnnotationParameterMap.put(index++, true); nonNullAnnotationCount++; } else { notNullAnnotationParameterMap.put(index++, false); } } if (nonNullAnnotationCount == 0) { continue; } 

该映射对我们很有用,然后调用该方法并将其null依次传递给带有批注@NonNull的所有参数,而不仅仅是第一个。

nonNullAnnotationCount参数计算该方法中应注释的参数数量@NonNull,它将确定每个方法的调用集成交互的次数。

顺便说一句,如果没有@NonNull注释(有参数,但是都是原始的或@Nullable),那么就没有什么可谈的了:

 if (nonNullAnnotationCount == 0) { continue; } 

我们手上有一个参数图。 我们知道调用一个方法多少次,并且在哪个位置可以为空,这是一件很小的事情(正如我天真的以为没有理解),我们需要创建该类的实例并在其上调用方法。

当您意识到实例的不同之处时,问题就开始了:它可以是一个私有类,它可以是一个带有一个默认构造函数的类,一个带有参数的构造函数,带有这样的构造函数,一个抽象类,一个接口(带有其默认方法,它们也是公共的)并且还需要进行测试)。

当我们通过钩子或弯钩构建实例时,我们需要将参数传递给invoke方法,并且在这里也是如此:如何创建最终类的实例? 和枚举? 和原始的? 以及一组基本元素(它也是一个对象,也可以添加注释)。

好吧,让我们按顺序进行。

第一种情况是具有一个私有构造函数的类:

 if (ONLY_ONE_PRIVATE_CONSTRUCTOR_FILTER.test(clazz)) { notNullAnnotationParameterMap.put(currentNullableIndex, false); method.invoke(clazz, invokeMethodParameterArray); makeErrorMessage(method); } 

然后我们只需调用我们的invoke方法,就将来自外部的clazz传递给测试,并传递一个参数数组,其中已使用注释@NonNull的标志将null填充到第一个位置(请记住,在上面我们创建了地图@ NonNulls),我们开始循环运行并创建参数数组,交替更改null参数的位置,并在调用该方法之前将标志清零,以便在下一次集成中另一个参数变为null。

在代码中,它看起来像这样:

 val invokeMethodParameterArray = new Object[parameterCurrentMethodArray.length]; boolean hasNullParameter = false; int currentNullableIndex = 0; for (int i = 0; i < invokeMethodParameterArray.length; i++) { if (notNullAnnotationParameterMap.get(i) && isFalse(hasNullParameter)) { currentNullableIndex = i; invokeMethodParameterArray[i] = null; hasNullParameter = true; } else { mappingParameter(parameterCurrentMethodArray[i], invokeMethodParameterArray, i); } } 

实例化的第一个选项已被挑选出来。

在其他接口上,不可能采用和创建接口的实例(它甚至没有构造函数)。

因此,使用该接口将如下所示:

 if (INTERFACE_FILTER.test(clazz)) { notNullAnnotationParameterMap.put(currentNullableIndex, false); method.invoke(createInstanceByDynamicProxy(clazz, invokeMethodParameterArray), invokeMethodParameterArray); makeErrorMessage(method); } 

如果createInstanceByDynamicProxy实现了至少一个接口,或者它本身是一个接口,则允许我们在类上创建实例

细微差别
请记住,从根本上讲,这是类实现的接口,类型接口(而不是某些Comparable)很重要,在该类中,您需要在目标类中实现方法,否则实例会让您惊讶于其类型

但是里面是这样的:

 private Object createInstanceByDynamicProxy(final Class clazz, final Object[] invokeMethodParameterArray) { return newProxyInstance( currentThread().getContextClassLoader(), new Class[]{clazz}, (proxy, method1, args) -> { Constructor<Lookup> constructor = Lookup.class .getDeclaredConstructor(Class.class); constructor.setAccessible(true); constructor.newInstance(clazz) .in(clazz) .unreflectSpecial(method1, clazz) .bindTo(proxy) .invokeWithArguments(invokeMethodParameterArray); return null; } ); } 

耙子
顺便说一下,这里也有一些耙子,我不记得了,有很多,但是您需要通过Lookup.class创建一个代理。

下一个实例(我的最爱)是一个抽象类。 在这里,动态代理将不再对我们有帮助,因为如果抽象类实现了某种类型的接口,那么显然这不是我们想要的类型。 就像那样,我们不能从抽象类中获取并创建newInstance()。 这里的CGLIB将为我们提供帮助,一个spring lib,它基于继承创建代理,但是麻烦的是,目标类必须具有默认的(没有参数)构造函数

八卦
尽管从春季4月开始通过互联网上的八卦来判断,但CGLIB可以在没有它的情况下运行,因此: 它不起作用!
实例化抽象类的选项是这样的:
 if (isAbstract(clazz.getModifiers())) { createInstanceByCGLIB(clazz, method, invokeMethodParameterArray); makeErrorMessage(); } 

在代码示例中已经看到的makeErrorMessage()会丢弃测试,如果我们使用带null的带注释的@NonNull参数调用该方法并且该方法没有失败,则该测试无效,您必须删除该测试。

对于参数映射,我们有一个通用的方法可以映射和锁定构造函数和方法参数,如下所示:

 private void mappingParameter(final Parameter parameter, final Object[] methodParam, final int index) throws InstantiationException, IllegalAccessException { if (isFinal(parameter.getType().getModifiers())) { if (parameter.getType().isEnum()) { methodParam[index] = Enum.valueOf( (Class<Enum>) (parameter.getType()), parameter.getType().getEnumConstants()[0].toString() ); } else if (parameter.getType().isPrimitive()) { mappingPrimitiveName(parameter, methodParam, index); } else if (parameter.getType().getTypeName().equals("byte[]")) { methodParam[index] = new byte[0]; } else { methodParam[index] = parameter.getType().newInstance(); } } else { methodParam[index] = mock(parameter.getType()); } } 

通常要注意Enum(蛋糕上的樱桃)的创建,一般来说,您不能仅仅采用并创建Enum。

对于最终参数,这里是您自己的映射,对于非最终参数是您自己的映射,然后只需在文本(代码)中即可。

好了,在为构造函数和方法创建参数之后,我们形成了实例:

 val firstFindConstructor = clazz.getConstructors()[0]; val constructorParameterArray = new Object[firstFindConstructor.getParameters().length]; for (int i = 0; i < constructorParameterArray.length; i++) { mappingParameter(firstFindConstructor.getParameters()[i], constructorParameterArray, i); } notNullAnnotationParameterMap.put(currentNullableIndex, false); createAndInvoke(clazz, method, invokeMethodParameterArray, firstFindConstructor, constructorParameterArray); makeErrorMessage(method); 

我们已经确定地知道,由于我们已经到了代码的这一阶段,这意味着我们至少有一个构造函数,我们可以使用任何一个构造函数来创建实例,因此我们采用我们看到的第一个实例,看看构造函数中是否有参数,如果没有,则调用像这样:

 method.invoke(spy(clazz.getConstructors()[0].newInstance()), invokeMethodParameterArray); 


好吧,如果有这样的事情:
 method.invoke(spy(clazz.getConstructors()[0].newInstance()), invokeMethodParameterArray); 

这是您看到的更高一点的createAndInvoke()方法中发生的逻辑。
扰流器下的测试类的完整版本,没有像我在一个工作项目中所写的那样上传到git,但实际上,它只是可以在您的测试中继承和使用的一个类。

源代码
 public class TestUtil { private static final Predicate<Method> METHOD_FILTER = method -> isPublic(method.getModifiers()) && isFalse(method.isSynthetic()) && isFalse(isAbstract(method.getModifiers())) && isFalse(method.getName().equals("equals")); private static final Predicate<Class> ONLY_ONE_PRIVATE_CONSTRUCTOR_FILTER = clazz -> clazz.getConstructors().length == 0 && isFalse(clazz.isInterface()); private static final Predicate<Class> INTERFACE_FILTER = clazz -> clazz.getConstructors().length == 0; private static final BiPredicate<Exception, Parameter> LOMBOK_ERROR_FILTER = (exception, parameter) -> isNull(exception.getCause().getMessage()) || isFalse(exception.getCause().getMessage().equals(parameter.getName())); protected void assertNonNullAnnotation(final Class clazz) throws Throwable { for (val method : getPublicMethods(clazz)) { if (method.getParameterCount() == 0) { continue; } int nonNullAnnotationCount = 0; int index = 0; val parameterCurrentMethodArray = method.getParameters(); val notNullAnnotationParameterMap = new HashMap<Integer, Boolean>(); for (val parameter : parameterCurrentMethodArray) { if (isNull(parameter.getAnnotation(Nullable.class)) && isFalse(parameter.getType().isPrimitive())) { notNullAnnotationParameterMap.put(index++, true); nonNullAnnotationCount++; } else { notNullAnnotationParameterMap.put(index++, false); } } if (nonNullAnnotationCount == 0) { continue; } for (int j = 0; j < nonNullAnnotationCount; j++) { val invokeMethodParameterArray = new Object[parameterCurrentMethodArray.length]; boolean hasNullParameter = false; int currentNullableIndex = 0; for (int i = 0; i < invokeMethodParameterArray.length; i++) { if (notNullAnnotationParameterMap.get(i) && isFalse(hasNullParameter)) { currentNullableIndex = i; invokeMethodParameterArray[i] = null; hasNullParameter = true; } else { mappingParameter(parameterCurrentMethodArray[i], invokeMethodParameterArray, i); } } try { if (ONLY_ONE_PRIVATE_CONSTRUCTOR_FILTER.test(clazz)) { notNullAnnotationParameterMap.put(currentNullableIndex, false); method.invoke(clazz, invokeMethodParameterArray); makeErrorMessage(method); } if (INTERFACE_FILTER.test(clazz)) { notNullAnnotationParameterMap.put(currentNullableIndex, false); method.invoke(createInstanceByDynamicProxy(clazz, invokeMethodParameterArray), invokeMethodParameterArray); makeErrorMessage(method); } if (isAbstract(clazz.getModifiers())) { createInstanceByCGLIB(clazz, method, invokeMethodParameterArray); makeErrorMessage(); } val firstFindConstructor = clazz.getConstructors()[0]; val constructorParameterArray = new Object[firstFindConstructor.getParameters().length]; for (int i = 0; i < constructorParameterArray.length; i++) { mappingParameter(firstFindConstructor.getParameters()[i], constructorParameterArray, i); } notNullAnnotationParameterMap.put(currentNullableIndex, false); createAndInvoke(clazz, method, invokeMethodParameterArray, firstFindConstructor, constructorParameterArray); makeErrorMessage(method); } catch (final Exception e) { if (LOMBOK_ERROR_FILTER.test(e, parameterCurrentMethodArray[currentNullableIndex])) { makeErrorMessage(method); } } } } } @SneakyThrows private void createAndInvoke( final Class clazz, final Method method, final Object[] invokeMethodParameterArray, final Constructor firstFindConstructor, final Object[] constructorParameterArray ) { if (firstFindConstructor.getParameters().length == 0) { method.invoke(spy(clazz.getConstructors()[0].newInstance()), invokeMethodParameterArray); } else { method.invoke(spy(clazz.getConstructors()[0].newInstance(constructorParameterArray)), invokeMethodParameterArray); } } @SneakyThrows private void createInstanceByCGLIB(final Class clazz, final Method method, final Object[] invokeMethodParameterArray) { MethodInterceptor handler = (obj, method1, args, proxy) -> proxy.invoke(clazz, args); if (clazz.getConstructors().length > 0) { val firstFindConstructor = clazz.getConstructors()[0]; val constructorParam = new Object[firstFindConstructor.getParameters().length]; for (int i = 0; i < constructorParam.length; i++) { mappingParameter(firstFindConstructor.getParameters()[i], constructorParam, i); } for (val constructor : clazz.getConstructors()) { if (constructor.getParameters().length == 0) { val proxy = Enhancer.create(clazz, handler); method.invoke(proxy.getClass().newInstance(), invokeMethodParameterArray); } } } } private Object createInstanceByDynamicProxy(final Class clazz, final Object[] invokeMethodParameterArray) { return newProxyInstance( currentThread().getContextClassLoader(), new Class[]{clazz}, (proxy, method1, args) -> { Constructor<Lookup> constructor = Lookup.class .getDeclaredConstructor(Class.class); constructor.setAccessible(true); constructor.newInstance(clazz) .in(clazz) .unreflectSpecial(method1, clazz) .bindTo(proxy) .invokeWithArguments(invokeMethodParameterArray); return null; } ); } private void makeErrorMessage() { fail("  @NonNull     DefaultConstructor  "); } private void makeErrorMessage(final Method method) { fail("    " + method.getName() + "   @NonNull"); } private List<Method> getPublicMethods(final Class clazz) { return Arrays.stream(clazz.getDeclaredMethods()) .filter(METHOD_FILTER) .collect(toList()); } private void mappingParameter(final Parameter parameter, final Object[] methodParam, final int index) throws InstantiationException, IllegalAccessException { if (isFinal(parameter.getType().getModifiers())) { if (parameter.getType().isEnum()) { methodParam[index] = Enum.valueOf( (Class<Enum>) (parameter.getType()), parameter.getType().getEnumConstants()[0].toString() ); } else if (parameter.getType().isPrimitive()) { mappingPrimitiveName(parameter, methodParam, index); } else if (parameter.getType().getTypeName().equals("byte[]")) { methodParam[index] = new byte[0]; } else { methodParam[index] = parameter.getType().newInstance(); } } else { methodParam[index] = mock(parameter.getType()); } } private void mappingPrimitiveName(final Parameter parameter, final Object[] methodParam, final int index) { val name = parameter.getType().getName(); if ("long".equals(name)) { methodParam[index] = 0L; } else if ("int".equals(name)) { methodParam[index] = 0; } else if ("byte".equals(name)) { methodParam[index] = (byte) 0; } else if ("short".equals(name)) { methodParam[index] = (short) 0; } else if ("double".equals(name)) { methodParam[index] = 0.0d; } else if ("float".equals(name)) { methodParam[index] = 0.0f; } else if ("boolean".equals(name)) { methodParam[index] = false; } else if ("char".equals(name)) { methodParam[index] = 'A'; } } } 


结论


这段代码可以在真实项目中工作并测试注释,而当所有内容都可以折叠时,目前只有一种选择。

在班级中声明一个Lombock塞特犬(如果有一个专家没有在Pojo班级中设置塞特犬,尽管那没有发生),并且宣告塞特犬的领域将不是最终的。

然后框架将友善地说一个公共方法,并且它有一个没有@NonNull批注的参数,解决方案很简单:显式声明setter并根据逻辑@NonNull / @ Nullable对其参数进行注释。

请注意,如果您希望我绑定到测试(或其他方式)中方法参数的名称,则在运行时中,默认情况下方法中的变量名称不可用,您会发现arg [0]和arg [1]等。 。
要在运行时中显示方法名称,请使用Maven插件:

 <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven.compiler.plugin.version}</version> <configuration> <source>${compile.target.source}</source/> <target>${compile.target.source}</target> <encoding>${project.build.sourceEncoding}</encoding> <compilerArgs><arg>-parameters</arg></compilerArgs> </configuration> </plugin> 

特别是此密钥:

 <compilerArgs><arg>-parameters</arg></compilerArgs> 

我希望你有兴趣。

Source: https://habr.com/ru/post/zh-CN464939/


All Articles