quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.48k stars 2.59k forks source link

Infinispan Extension: Allow InjectMock for RemoteCache #30450

Closed jonsalvas closed 5 months ago

jonsalvas commented 1 year ago

Description

Right now, when doing

class InfinispanCacheTest {
@InjectMock RemoteCache remoteCache;
...

I receive the following error:

org.junit.jupiter.api.extension.TestInstantiationException: Failed to create test instance

    at io.quarkus.test.junit.QuarkusTestExtension.initTestState(QuarkusTestExtension.java:773)
    at io.quarkus.test.junit.QuarkusTestExtension.interceptTestClassConstructor(QuarkusTestExtension.java:737)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.api.extension.InvocationInterceptor.interceptTestClassConstructor(InvocationInterceptor.java:73)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
    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.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:62)
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestClassConstructor(ClassBasedTestDescriptor.java:363)
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateTestClass(ClassBasedTestDescriptor.java:310)
    at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.instantiateTestClass(ClassTestDescriptor.java:79)
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcessTestInstance(ClassBasedTestDescriptor.java:286)
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$4(ClassBasedTestDescriptor.java:278)
    at java.base/java.util.Optional.orElseGet(Optional.java:369)
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$5(ClassBasedTestDescriptor.java:277)
    at org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:31)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$prepare$0(TestMethodTestDescriptor.java:105)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:104)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:68)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$2(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:90)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
    at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
    at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
    at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: java.lang.IllegalStateException: Invalid use of io.quarkus.test.junit.mockito.InjectMock - could not resolve the bean of type: org.infinispan.client.hotrod.RemoteCache. Offending field is remoteCache of test class class xxxxxxxx.InfinispanCacheTest
    at io.quarkus.test.junit.mockito.internal.CreateMockitoMocksCallback.getBeanInstance(CreateMockitoMocksCallback.java:107)
    at io.quarkus.test.junit.mockito.internal.CreateMockitoMocksCallback.afterConstruct(CreateMockitoMocksCallback.java:36)
    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(Method.java:566)
    at io.quarkus.test.junit.AbstractTestWithCallbacksExtension.invokeCallbacks(AbstractTestWithCallbacksExtension.java:154)
    at io.quarkus.test.junit.AbstractTestWithCallbacksExtension.invokeAfterConstructCallbacks(AbstractTestWithCallbacksExtension.java:64)
    at io.quarkus.test.junit.QuarkusTestExtension.initTestState(QuarkusTestExtension.java:767)
    ... 66 more

It would be great if mocking of RemoteCache would be possible so that we test infispan-consuming classes.

I know you can use Dev-Server for testing, but this is not an option for us, as the CI environment does not support testcontainers.

Implementation ideas

No response

quarkus-bot[bot] commented 1 year ago

/cc @gwenneg (cache), @karesti (infinispan), @wburns (infinispan)

karesti commented 1 year ago

taking the point

ractoc commented 1 year ago

aside from not being able to run containers on a test environment, being able to mock infinispan would also mean faster unit tests as no container needs to ben built / started. Currently, we fix this situation by having a permanent test instance of infinispan availlable and our tests just start by wiping the caches. But this is off course not ideal since our tests now fail if for some reason that infinispan instance is down.

karesti commented 1 year ago

The beans of scope @Dependent or @Singleton can't be used with InjectMock

The RemoteCache can't be ApplicationScoped since it would break the code of Search at this moment:

QueryFactory queryFactory = Search.getQueryFactory(cache);

We need to make modifications to the Infinispan implementation to make it possible, and I'm not sure we can do that easily or without impact. We need to check this @wburns

However, RemoteCacheManager and CounterManager can be @ApplicationScoped beans, even if they are @Dependent beans now. This evolution to ApplicationScoped beans is landing here: https://github.com/quarkusio/quarkus/pull/32494 You will be able to mock getCache and use your own RemoteCache mocks if you use the injected RemoteCacheManager to grab the RemoteCache instance instead of using directly the RemoteCache injection. In the new version, RemoteCache will be @Singleton

@ractoc @jonsalvas

ractoc commented 1 year ago

@karesti I'm currently trying to implement your suggestion. This is the code I'm using atm: Service implementation:

@ApplicationScoped
public class ComponentService {

    public static final String CACHE_PVB_CORE_COMP = "pvbCoreComp";

    @Inject
    @ApplicationScoped
    RemoteCacheManager cacheManager;

    public Optional<Component> findComponentById(String componentId) {
        RemoteCache<String, Component> cache = cacheManager.getCache(CACHE_PVB_CORE_COMP);
        return Optional.ofNullable(cache.get(componentId));
    }
}

Service test implementation:

@QuarkusTest
class ComponentServiceTest {

    @Inject
    ComponentService service;

    @InjectMock
    RemoteCacheManager remoteCacheManagerMock;

    @org.mockito.Mock
    RemoteCache mockedCache;

    @Test
    void findComponentById() {
        // Given
        when(mockedCache.get("componentId")).thenReturn(new Component("testComponent", "testComponentId"));
        when(remoteCacheManagerMock.getCache("pvbCoreComp")).thenReturn(mockedCache);

        // When
        Optional<Component> component = service.findComponentById("componentId");

        // Then
        assertThat(component)
                .isNotEmpty()
                .get()
                .hasFieldOrPropertyWithValue("componentId", "testComponentId")
                .hasFieldOrPropertyWithValue("name", "testComponent");
    }
}

While I am using @InjectMock to inject my mocked RemoteCacheManager, I keep getting the error:

Cannot invoke "org.infinispan.client.hotrod.RemoteCacheManager.getCache(String)" because "this.cacheManager" is null
karesti commented 1 year ago

@ractoc I probably did not explain well. In the current extension, RemoteCacheManager is a dependant scope bean. The transformation of the RemoteCacheManager in an application scoped bean is already developed in this pull request. https://github.com/quarkusio/quarkus/pull/32494 Once the feature is merged, you will be able to inject and use it as mock

ractoc commented 1 year ago

coo, yeah I can now mock my cachemanager and through that the RemoteCache itsself. Next up, trying to figure out how to make this all work with the QueryFactory...

karesti commented 1 year ago

@ractoc I'll see with the team how to improve the experience

ractoc commented 1 year ago

In the end the solution was fairly simple, if a bit ugly. The only way I could really get it to work is to create a test stub for the Search object in my test code with the same package / Class as the original. Then I just have that one return my Query mock object as needed. As I said, it's ugly, but it works. The problem is, that the Search object only has 2 static methods, so they are hard to mock. This IS possible, but you need to replace the mockito-core with mockito-inline. Since the mockito-core comes from the quarkus-junit5-mockito dependency...

fax4ever commented 1 year ago

Hi @karesti, I created ISPN-15060 to improve the current Infinispan API to use a proxy to get query factory. Thank you for raising this issue. It is something that we need to address soon I think.