Mockito 不理解在反射方法上调用 method.invoke()

Mockito doesn't understand calling method.invoke() on reflected Method

这个问题非常复杂,这种行为对我来说毫无意义。

我有这个 class:

public class TestMethodsProvider extends AnnotatedCommandExecutor<JavaPlugin> {
    public TestMethodsProvider(final CommandSender sender, final JavaPlugin plugin) {
        super(sender, plugin);
    }

    @Argument
    public void testMethodWithParam(@Mapper("testWorldMapper") final World world) {
    }

    @ArgumentFallback("testWorldMapper")
    public void testMethodWithParamArgumentFallback(final String world) {
    }
}

基本行为如下:

  1. 引擎反射性地调用 testMethodWithParam
  2. 该方法具有复杂的参数,因此引擎尝试从原始字符串创建 World 对象(由于 @Mapper("testWorldMapper") 注释)。
  3. 映射失败(创建 null),因此应调用回退方法(如果存在)。
  4. 引擎搜索 @ArgumentFallback("testWorldMapper") 的 class(提供的文本必须与 @Mapper("testWorldMapper") 中的相同)
  5. 如果找到该方法,将使用即将成为 World 且失败的文本调用它。

第 1-2 点现在并不重要,它们仅用于理解行为。

我有一个 class 设计来完成第 3-5 点:

public class FallbackInvoker<E extends JavaPlugin> {
    @NotNull
    public Object invokeFallback(@Nullable final Mapper mapper,
                                 @NotNull final String text,
                                 @NotNull final Class<?> targetClass,
                                 @NotNull final AnnotatedCommandExecutor<E> executor) {
        return Optional.ofNullable(mapper)
                .map(x -> getArgumentFallbackMethods(x.value(), executor))
                .or(() -> Optional.of(getTypeFallbackMethods(targetClass, executor)))
                .stream()
                .flatMap(Collection::stream)
                .map(method -> invokeMethod(method, executor, text))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    @NotNull
    private List<Method> getArgumentFallbackMethods(@NotNull final String argument,
                                                    @NotNull final AnnotatedCommandExecutor<E> executor) {
        return Arrays.stream(executor.getClass().getDeclaredMethods())
                .filter(method -> method.isAnnotationPresent(ArgumentFallback.class))
                .filter(method -> Arrays.asList(method.getAnnotation(ArgumentFallback.class).value())
                        .contains(argument))
                .filter(method -> method.getParameterCount() == 1)
                .sorted((o1, o2) -> o2.getAnnotation(ArgumentFallback.class).priority().getSlot()
                        - o1.getAnnotation(ArgumentFallback.class).priority().getSlot())
                .collect(Collectors.toList());
    }

    @NotNull
    private List<Method> getTypeFallbackMethods(@NotNull final Class<?> type,
                                                @NotNull final AnnotatedCommandExecutor<E> executor) {
        return Arrays.stream(executor.getClass().getDeclaredMethods())
                .filter(method -> method.isAnnotationPresent(TypeFallback.class))
                .filter(method -> Arrays.asList(method.getAnnotation(TypeFallback.class).value())
                        .contains(type))
                .filter(method -> method.getParameterCount() == 1)
                .sorted((o1, o2) -> o2.getAnnotation(TypeFallback.class).priority().getSlot()
                        - o1.getAnnotation(TypeFallback.class).priority().getSlot())
                .collect(Collectors.toList());
    }

    @SneakyThrows
    private Object invokeMethod(@NotNull final Method method,
                                @NotNull final AnnotatedCommandExecutor<E> executor,
                                @NotNull final String text) {
        return method.invoke(executor, text); // THIS LINE IS IMPORTANT
    }
}

测试看起来像这样:

    @Test
    void theTest() throws ReflectiveOperationException {
        final JavaPlugin plugin = mock(JavaPlugin.class);
        final CommandSender sender = mock(CommandSender.class);
        final IFallbackInvoker<JavaPlugin> invoker = new FallbackInvoker<>();
        final Class<? extends TestMethodsProvider> providerClass = TestMethodsProvider.class;
        final Method fallbackMethod = spy(providerClass.getDeclaredMethod("testMethodWithParamArgumentFallback",
                String.class));
        final Method method = providerClass.getDeclaredMethod("testMethodWithParam", World.class);
        final TestMethodsProvider testMethodsProvider = (TestMethodsProvider) providerClass.getConstructors()[0]
                .newInstance(sender, plugin);
        final Mapper mapper = method.getParameters()[0].getAnnotation(Mapper.class);

        final Object result = invoker.invokeFallback(mapper, "test", World.class, testMethodsProvider);

        assertEquals(new ArrayList<>(), result);
        verify(fallbackMethod, times(1)).invoke(any(), any());
    }

一切看起来都与事实完全不同......它失败了。消息是:

Wanted but not invoked:
method.invoke(<any>, <any>);
-> at java.base/java.lang.reflect.Method.invoke(Method.java:556)
Actually, there were zero interactions with this mock.

Wanted but not invoked:
method.invoke(<any>, <any>);
-> at java.base/java.lang.reflect.Method.invoke(Method.java:556)
Actually, there were zero interactions with this mock.

    at java.base/java.lang.reflect.Method.invoke(Method.java:556)
    at eu.andret.arguments.mapper.FallbackInvokerTest.dupa(FallbackInvokerTest.java:54)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Unknown Source)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
    at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod[=14=](ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke[=14=](ExecutableInvoker.java:105)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod(TestMethodTestDescriptor.java:210)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:248)
    at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute(DefaultLauncher.java:211)
    at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:226)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:199)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:132)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access[=14=]0(JUnitPlatformTestClassProcessor.java:79)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Unknown Source)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
    at com.sun.proxy.$Proxy5.stop(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.stop(TestWorker.java:133)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Unknown Source)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
    at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
    at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
    at org.gradle.internal.concurrent.ManagedExecutorImpl.run(ManagedExecutorImpl.java:48)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
    at java.base/java.lang.Thread.run(Thread.java:834)

失败意味着没有调用上面指出的行。但是当我在那里执行 System.out.println(method); 时,它会准确地打印出我想要调用的方法。我还尝试调试它并且调试器停在那里,因此应该调用该方法。为什么不是?

我想补充一点我有一个类似的测试(用于测试正确的映射)效果很好:

    @Test
    void invokeFallbackMethodWith() throws ReflectiveOperationException {
        // given
        final JavaPlugin plugin = mock(JavaPlugin.class);
        final CommandSender sender = mock(CommandSender.class);
        abstract class LocalFunction implements Function<String, World> {
        }
        abstract class LocalFallbackInvoker extends FallbackInvoker<JavaPlugin> {
        }
        final Function<String, World> getWorld = mock(LocalFunction.class);
        final MappingConfig mappingConfig = new MappingConfig();
        mappingConfig.add("testWorldMapper", new MappingSet<>(World.class, getWorld, TypeFallback.ALWAYS));
        final IFallbackInvoker<JavaPlugin> fallbackInvoker = mock(LocalFallbackInvoker.class);
        final IMethodInvoker<JavaPlugin> invoker = new MethodInvoker<>(plugin, mappingConfig, fallbackInvoker);
        final Class<? extends AnnotatedCommandExecutor<JavaPlugin>> provider = TestMethodsProvider.class;
        final Method methodWorld = spy(provider.getDeclaredMethod("testMethodWithParam", World.class));
        final Mapper mapper = methodWorld.getParameters()[0].getAnnotation(Mapper.class);
        final TestMethodsProvider o = (TestMethodsProvider) provider.getConstructors()[0].newInstance(sender, plugin);

        // when
        invoker.invokeMethod(methodWorld, new String[]{"testMethod", "test"}, sender, provider);

        // then
        verify(methodWorld, times(0)).invoke(any());
        verify(fallbackInvoker, times(1)).invokeFallback(mapper, "test", World.class, o);
    }

然后我现在完全不明白。为什么一个测试通过而另一个没有通过?怎样做才能使测试通过?手动测试时,一切正常,只有测试有一些我没有看到的问题。

当使用 Mockito 的 verify() 时,所有调用都必须对由 mock()spy() 创建的实例进行,否则它们将无法被 Mockito 记录。

在您的第一个测试中,您永远不会将创建的间谍传递给 invokeFallback() 函数,因此 Mockito 不知道您正在进行的调用。

在您的第二个测试(工作测试)中,您正在调用 spy() 返回的方法,这就是它起作用的原因。