vendredi 3 septembre 2021

Mockito doesn't understand calling method.invode() on spied reflect Method

Ok, so... The problem is highly complicated, and the behavior makes no sense to me.

I have the 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) {
    }
}

The basic behavior is following:

  1. The engine reflectively is going to call testMethodWithParam.
  2. The method has complex parameter, so the engine tries to create the World object from an original String (due to @Mapper("testWorldMapper") annotation).
  3. Mapping fails (creates null), so the fallback method should be called if present.
  4. The engine searches through the class of the @ArgumentFallback("testWorldMapper") (the text provided must be the same as in @Mapper("testWorldMapper"))
  5. If the method is found, it's being invoked with the text that was about to become the World and failed.

Points 1-2 aren't important now, they are only for understanding the behavior.

I have a class designed to do points 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
    }
}

And the test looks like this:

    @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());
    }

Everything looks perfectly apart the fact... it fails. The message is:

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$0(ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(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$6(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$5(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$7(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$8(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$5(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$7(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$8(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$5(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$7(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$8(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$5(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$000(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$1.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)

The failure would mean the pointed line above wasn't called. But when I does a System.out.println(method); there, it prints exactly the method I want to be called. I also tried to debug it and the debugger stops there, so the method should be invoked. Why isn't it?

I'd like to add I have a similar test (for testing correct mapping) that works well:

    @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);
    }

And now I don't understand it at all. Why one test passes and the other doesn't? What to do to make test to pass? When manual testing, everything is working, only tests have some problem I don't see.





Aucun commentaire:

Enregistrer un commentaire