quarkus-cxf icon indicating copy to clipboard operation
quarkus-cxf copied to clipboard

Testing: using @InjectMock with @CXFClient leads to NPE

Open mickroll opened this issue 3 years ago • 24 comments

Given example code that uses a generated ExamplePortType (any should suffice):

import com.example.generated.ExamplePortType;
import io.quarkiverse.cxf.annotation.CXFClient;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;

@QuarkusTest
class ExamplePortTypeInjectMockTest {

    @InjectMock
    @CXFClient
    ExamplePortType examplePortTypeMock;

    @Test
    void testName() {
        // nothing to do
    }
}

Executing this test should be okay, but it leads to a NPE:

org.junit.jupiter.api.extension.TestInstantiationException: Failed to create test instance
	at io.quarkus.test.junit.QuarkusTestExtension.initTestState(QuarkusTestExtension.java:828)
	at io.quarkus.test.junit.QuarkusTestExtension.interceptTestClassConstructor(QuarkusTestExtension.java:792)
	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.api.extension.InvocationInterceptor.interceptTestClassConstructor(InvocationInterceptor.java:72)
	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:77)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestClassConstructor(ClassBasedTestDescriptor.java:342)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateTestClass(ClassBasedTestDescriptor.java:289)
	at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.instantiateTestClass(ClassTestDescriptor.java:79)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcessTestInstance(ClassBasedTestDescriptor.java:267)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$2(ClassBasedTestDescriptor.java:259)
	at java.base/java.util.Optional.orElseGet(Optional.java:369)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$3(ClassBasedTestDescriptor.java:258)
	at org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:31)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$prepare$0(TestMethodTestDescriptor.java:101)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:100)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:65)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$1(NodeTestTask.java:111)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:111)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:79)
	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.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:84)
	at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:98)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:40)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:541)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:768)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:464)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)
Caused by: java.lang.reflect.InvocationTargetException
	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.QuarkusTestExtension.initTestState(QuarkusTestExtension.java:820)
	... 63 more
Caused by: java.lang.NullPointerException
	at io.quarkiverse.cxf.CxfClientProducer.selectorCXFClientInfo(CxfClientProducer.java:181)
	at io.quarkiverse.cxf.CxfClientProducer.loadCxfClient(CxfClientProducer.java:58)
	at com.example.generated.ExamplePortTypeCxfClientProducer.createService(ExamplePortTypeCxfClientProducer.zig:39)
	at com.example.generated.ExamplePortTypeCxfClientProducer_ProducerMethod_createService_e6a6e5534d8080ede4e8215852f246f21a2a3dfb_Bean.create(ExamplePortTypeCxfClientProducer_ProducerMethod_createService_e6a6e5534d8080ede4e8215852f246f21a2a3dfb_Bean.zig:210)
	at com.example.generated.ExamplePortTypeCxfClientProducer_ProducerMethod_createService_e6a6e5534d8080ede4e8215852f246f21a2a3dfb_Bean.get(ExamplePortTypeCxfClientProducer_ProducerMethod_createService_e6a6e5534d8080ede4e8215852f246f21a2a3dfb_Bean.zig:240)
	at com.example.generated.ExamplePortTypeCxfClientProducer_ProducerMethod_createService_e6a6e5534d8080ede4e8215852f246f21a2a3dfb_Bean.get(ExamplePortTypeCxfClientProducer_ProducerMethod_createService_e6a6e5534d8080ede4e8215852f246f21a2a3dfb_Bean.zig:275)
	at io.quarkus.arc.impl.ArcContainerImpl.beanInstanceHandle(ArcContainerImpl.java:430)
	at io.quarkus.arc.impl.ArcContainerImpl.beanInstanceHandle(ArcContainerImpl.java:443)
	at io.quarkus.arc.impl.ArcContainerImpl.instanceHandle(ArcContainerImpl.java:413)
	at io.quarkus.arc.impl.ArcContainerImpl.instance(ArcContainerImpl.java:222)
	at io.quarkus.test.junit.mockito.internal.CreateMockitoMocksCallback.getBeanInstance(CreateMockitoMocksCallback.java:72)
	at io.quarkus.test.junit.mockito.internal.CreateMockitoMocksCallback.afterConstruct(CreateMockitoMocksCallback.java:28)
	... 68 more

My expactation would be, that a mockito mock of ExamplePortType is injected into the test and all other beans.

mickroll avatar Jun 10 '21 09:06 mickroll

check if InjectionPoint ip is not null in io.quarkiverse.cxf.CxfClientProducer.selectorCXFClientInfo

dufoli avatar Jun 11 '21 09:06 dufoli

check if InjectionPoint ip is not null in io.quarkiverse.cxf.CxfClientProducer.selectorCXFClientInfo

@dufoli are you asking @mickroll to check that or is that a note to yourself for implementing the fix? Sorry if that's a stupid question. 😉

famod avatar Jun 13 '21 12:06 famod

Note for implementing fix. But maybe I have to dig deeper to understand why it is null...

dufoli avatar Jun 13 '21 21:06 dufoli

can you test this branch. I guess you have an injection point without annotation CxfClient and it make it bug.

dufoli avatar Jun 15 '21 07:06 dufoli

another thing, I read that may be the root cause if it do not worked.

https://quarkus.io/guides/cdi it talk about monking and using ApplicationScoped in order to mock instead of singleton. It is what we do but it seems to be used with QuarkusMock quarkus mock Can you switch to

@QuarkusTest
class ExamplePortTypeInjectMockTest {

    @Inject
    @CXFClient
    ExamplePortType examplePortTypeMock;

   @BeforeAll
    public static void setup() {
        ExamplePortType mock = Mockito.mock(ExamplePortType.class);
        //Mockito.when(mock.greet("Stuart")).thenReturn("A mock for Stuart");
       // mock all needed return here
        QuarkusMock.installMockForType(mock, ExamplePortType.class);  
    }

    @Test
    void testName() {
        // nothing to do
    }
}

dufoli avatar Jun 15 '21 07:06 dufoli

I'll try that within the next days. Sorry, current project is on a tight schedule.

mickroll avatar Jun 15 '21 19:06 mickroll

Finally found some time to try you suggestion.

Can you switch to

With this approach, installMockForType fails in @BeforeAll:

javax.enterprise.inject.UnsatisfiedResolutionException: No bean found for required type [interface com.example.ExamplePortType] and qualifiers [[]]
	at io.quarkus.arc.impl.InstanceImpl.bean(InstanceImpl.java:175)
	at io.quarkus.arc.impl.InstanceImpl.getInternal(InstanceImpl.java:196)
	at io.quarkus.arc.impl.InstanceImpl.get(InstanceImpl.java:93)
	at io.quarkus.test.junit.QuarkusMock.installMockForType(QuarkusMock.java:57)
	at com.example.ExamplePortTypeInjectMockTest.setup(ExamplePortTypeInjectMockTest.java:32)
	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.QuarkusTestExtension.runExtensionMethod(QuarkusTestExtension.java:944)
	at io.quarkus.test.junit.QuarkusTestExtension.interceptBeforeAllMethod(QuarkusTestExtension.java:734)
	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)

Btw. using installMockForType in @BeforeEach yields the same problem.

mickroll avatar Jun 28 '21 05:06 mickroll

can you test this branch. I guess you have an injection point without annotation CxfClient and it make it bug.

The NPE is gone. But the test still won't run. I'll try to create a reproducer for the rest.

mickroll avatar Jun 28 '21 06:06 mickroll

Pushed a demonstrator, see above. This is what i want to achieve:

Service -> cxfWsClient
Test -> Service, with mocked cxfWsClient

Btw. I couldn't get any tests to run, always failing with IllegalStateException: No root definition found for class io.quarkiverse.cxf.CxfConfig on a fresh master pull with mvn clean install.

mickroll avatar Jun 28 '21 06:06 mickroll

@mickroll hmm.... what is properties of your service. https://github.com/mickroll/quarkus-cxf/blob/f75dd786175135ba6466bf9c4aa6e959e2c2c44d/integration-tests/src/main/resources/application.properties there are few service and they are all named. can you try with @CXFClient("mockCalculator")

dufoli avatar Jun 28 '21 08:06 dufoli

can you try with @CXFClient("mockCalculator")

Still fails like every other test with IllegalStateException: No root definition found for class io.quarkiverse.cxf.CxfConfig.

Somehow my approach for running all integration tests seems to be wrong:

$ git clone ...
$ mvn clean install

-> all tests fail with IllegalStateException: No root definition found for class io.quarkiverse.cxf.CxfConfig, even on master.

with:

$ mvn --version
Apache Maven 3.8.1 (05c21c65bdfed0f71a2f2ada8b84da59348c4c5d)
Maven home: [...]
Java version: 11.0.11, vendor: AdoptOpenJDK, runtime: [...]
Default locale: de_DE, platform encoding: Cp1252
OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"

mickroll avatar Jun 28 '21 09:06 mickroll

ok got it indeed there is some code on quarkus core to handle injectMock attribute. So we have to handle it too.

https://github.com/quarkusio/quarkus/blob/main/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/CreateMockitoMocksCallback.java

commit to introduce support for it: https://github.com/quarkusio/quarkus/commit/96db1d725d689f441bcf674741ae974a3cf2c90b#diff-0f49776425f1d405da03781ad19d8d1f2b935976463c4e8cdb66a4ee8c37ece6

here is class to add the support for it: https://github.com/quarkusio/quarkus/blob/main/test-framework/junit5-mockito/src/main/java/io/quarkus/test/junit/mockito/internal/CreateMockitoMocksCallback.java

so the issue is indeed that when you inject your mock it created bean instance first and use the regular ay before overwriting it. But for that we need to have configuration and there are no root node for client configuration.

https://github.com/mickroll/quarkus-cxf/commit/f75dd786175135ba6466bf9c4aa6e959e2c2c44d

SO

CXFClientInfo must be injected with a dummy for mock I guess. Best maybe is to have a default CXFClientInfo with convention properties instead of configuration so no exception thrown. It will fix mock and raise configuration over configuration for default client (localthost url, ... we can add it in: CxfClientProducer.produceCxfClient if we do so we need the patch I cancel to check if injectionPoint is null too

@shumonsharif @famod are you ok for the approach ?

dufoli avatar Jun 28 '21 10:06 dufoli

@dufoli I was never able to successfully mock a CXF / JAX-WS client in general (even outside of the Quarkus realm), so I'm not entirely sure how this will work without an actual endpoint to process the request.

I'm also not entirely clear on the requirement ... I'm guessing we are trying to return a pre-canned static response for the mock client, and don't want it to make an HTTP call?

shumonsharif avatar Jun 28 '21 11:06 shumonsharif

I'm also not entirely clear on the requirement ... I'm guessing we are trying to return a pre-canned static response for the mock client, and don't want it to make an HTTP call?

Yes, maybe i should have made that clearer in the first place. Sorry for that.

What i have is a service that uses an injected CxfClient to connect to an external webservice. Now i want to be able to test all the services logic, without doing the external request. Instead of the CxfClient i want to have a mockito mock injected into the service, so i can check its input and control its output. Just like any service-layer-test where you mock one bean to test another. This way i would also have a very slim (fast) test, without a lot of other dependencies. What i don't want is that a real http request is made to some kind of mock. The latter would require a lot of configuration for (eg. disabling) tls / auth / request logging / ...

mickroll avatar Jun 28 '21 13:06 mickroll

I have worked a little bit on it. indeed, we have an issue because

Caused by: java.lang.RuntimeException: org.apache.cxf.jaxws.JaxWsClientProxy@20a81a61 is not a normal scoped CDI bean, make sure the bean is a normal scope like @ApplicationScoped or @RequestScoped. Alternatively you can use '@InjectMock(convertScopes=true)' instead of '@InjectMock' if you would like Quarkus to automatically make that conversion (you should only use this if you understand the implications).
        at io.quarkus.test.junit.mockito.internal.SetMockitoMockAsBeanMockCallback.installMock(SetMockitoMockAsBeanMockCallback.java:18)

And the issue is that sei (web service interface is not ApplicationScoped. And this class is generated by cxf not by quarkus. On resteasy, I found a bug with same pattern and they fixed it by added @AppicationScoped on interface: https://github.com/quarkusio/quarkus/issues/8622 but because it is done on cxf part, I do not think we can do that. Or maybe by enriching class generated by cxf. I have tried to :

  • add org.mockito.plugins.MockMaker resource file without success
  • add @InjectMock(convertScopes = true) without success

dufoli avatar Jun 28 '21 13:06 dufoli

Hi @mickroll @famod @dufoli I may have put together a fairly trivial workaround to address this; it leverages custom injection of fields into the test classes, and is generic enough to be applied to mock any CXFClient instance.

Would you kindly share any thoughts on this; whether it is a feasible workaround? Does it address the core concerns?

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectMockCXFClient {

}
import io.quarkus.test.common.QuarkusTestResourceConfigurableLifecycleManager;
import org.mockito.Mockito;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Map;

public class MockCXFClientTestResource implements QuarkusTestResourceConfigurableLifecycleManager {

    @Override
    public Map<String, String> start() {
        return Collections.emptyMap();
    }

    @Override
    public void stop() {

    }

    @Override
    public void inject(Object testInstance) {
        Class<?> c = testInstance.getClass();
        while (c != Object.class) {
            for (Field f : c.getDeclaredFields()) {
                Annotation ano = null;
                for (Annotation annotation : f.getAnnotations()) {
                    if (InjectMockCXFClient.class.getName().equals(annotation.annotationType().getName())) {
                        ano = annotation;
                    }
                }
                if (ano != null) {
                    f.setAccessible(true);
                    try {
                        f.set(testInstance, Mockito.mock(f.getType()));
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            c = c.getSuperclass();
        }
    }

}
@QuarkusTest
@QuarkusTestResource(MockCXFClientTestResource.class)
public class CalculatorSoapTest {

    @InjectMockCXFClient
    CalculatorSoap mockCalculatorSoap;

    @BeforeEach
    void setup() {
        doReturn(23).when(mockCalculatorSoap).subtract(anyInt(), anyInt());
        doReturn(23).when(mockCalculatorSoap).divide(anyInt(), anyInt());
        doReturn(23).when(mockCalculatorSoap).add(anyInt(), anyInt());
        doReturn(23).when(mockCalculatorSoap).multiply(anyInt(), anyInt());
    }

    @Test
    void testMultiply() {
        Assertions.assertEquals(23, mockCalculatorSoap.multiply(13, 17));
    }

    @Test
    void testAdd() {
        Assertions.assertEquals(23, mockCalculatorSoap.add(19, 23));
    }
}

shumonsharif avatar Nov 23 '21 13:11 shumonsharif

@shumonsharif Thanks for taking a look!

I haven't tried it out, but I don't think it would replace the respective client in other classes than the test class? Imagine a Connector class in main that gets the client injected and that provides some coarser grained methods, e.g. to avoid soap/jaxb binding classes from leaking into the core domain of your app. What you typically want to do (well, we do) is to test the Connector, but with a mocked client.

Two more remarks:

  • @QuarkusTestResource runs for all tests (might be ok or not) and @QuarkusTestResource.restrictToAnnotatedClass triggers a Quarkus reboot (runtime penalty)
  • Since a few Quarkus versions, QuarkusTestResourceLifecycleManager has a more convenient void inject(TestInjector testInjector)

famod avatar Nov 24 '21 21:11 famod

Makes sense @famod; what I proposed would only work for simper use cases, such as injecting respective clients directly into the test classes. Will definitely need to give this some deeper thought - as @dufoli mentioned previously, CXF is generating the classes we'd need to enrich, which just seems cumbersome.

shumonsharif avatar Nov 25 '21 10:11 shumonsharif

@famod Interestingly enough, I just tried out the use case you described using @InjectMocks and it appears to work with the proposed workaround. Assuming we have the following class:

public class CalculatorService {

    @Inject
    @CXFClient("calculator-soap12-client")
    CalculatorSoap calculatorSoap2;

    public int add(int intA, int intB) {
        return calculatorSoap2.add(intA, intB);
    }

}

The following test case works with the mocked CXFClient (I was not expecting it to previously due to the custom @InjectMockCXFClient annotation):

@QuarkusTest
@QuarkusTestResource(MockCXFClientTestResource.class)
public class CalculatorSoapTest {

    @InjectMockCXFClient
    @CXFClient("calculator-soap12-client")
    CalculatorSoap mockCalculatorSoap2;

    @InjectMocks
    CalculatorService calculatorService = new CalculatorService();

    @BeforeEach
    void setup() {
        MockitoAnnotations.openMocks(this);
        doReturn(23).when(mockCalculatorSoap2).add(anyInt(), anyInt());
    }

    @Test
    void testAdd() {
        Assertions.assertEquals(23, calculatorService.add(2, 3));
    }
}

Would you kindly take a look once you have some time? I think documenting this simple workaround should suffice assuming the @InjectMocks will address the use case you were describing?

shumonsharif avatar Nov 28 '21 15:11 shumonsharif

I should have been more precise:

Yeah, when doing the injection "manually", this will work:

    @InjectMocks
    CalculatorService calculatorService = new CalculatorService();

What I meant won't work is:

    @Inject
    CalculatorService calculatorService;

Here, ArC will use the real client, not the mocked one (because it doesn't know about the mock).

The @InjectMocks workaround can become more complicated/verbose when there are other non-mocked beans that need to be injected into CalculatorService. Furthermore, calculatorService will not receive any interceptor bindings etc. when not created via ArC (obviously, but this might not be clear to every user).

It's a workaround that is worth documenting, but has a few drawbacks (as is the nature of workarounds) that should be mentioned.

famod avatar Nov 28 '21 17:11 famod

I need to take more time to have a longer look, but it appears to me that we need the InjectionPoint (which is forcing us to use @Dependent) "only" to detect wheter @CXFClient is present: https://github.com/quarkiverse/quarkus-cxf/blob/70838322872be170ce2942901989ac2d264499fc/runtime/src/main/java/io/quarkiverse/cxf/CxfClientProducer.java#L181-L183

So I've been thinking: What if we collect that info during build time and then produce an @ApplicationScoped (!) bean per found @CXFClient? Of course, this would be a rather big change with some implications.

famod avatar Nov 29 '21 23:11 famod

Hey @famod I think this may work, and possibly our best option - it is also indeed a major change with impacts to existing functionality. I will also try to dig deeper based on your suggestion.

I believe impact wise with switching to @ApplicationScoped we may need to watch out for some of the issues described here: https://cxf.apache.org/faq.html#FAQ-AreJAX-WSclientproxiesthreadsafe%3F

Were there any other possible implications you were thinking?

shumonsharif avatar Dec 01 '21 12:12 shumonsharif

Yes, thread-safety is one concern. I actually used thread.local.request.context for a long time in a previous project.

Another implication is that unwrapping the client proxy and modifying e.g. the Conduit in a @PostConstruct of one bean will then have a global effect. Of course, this isn't relevant if the client is only injected in one place. Speaking of ClientProxy, I suppose people would need to unwrap the ArC proxy first, and only then use ClientProxy.getClient(). If that's the case, documentation needs to reflect that and ideally a small util should be provided to make things easier.

Finally, that change would close the door for "injection point specific settings". Or let's say...it would make it much more difficult. In a previous project (based on JBoss EAP) @mickroll and I developed a small SOAP client producer framework that allowed a couple of settings per injection point and it worked rather well. All clients were @Dependent (obviously). At some point, to save memory and creation time, we introduced a static cache to produce/reuse the same client instance for each injection point that had the same config as another one. So this is doable, but you need to properly choose all the config key parts and the end this was actually undermining the @Dependent contract, TBH.

famod avatar Dec 01 '21 23:12 famod

Any progress on this issue ?

maag03 avatar Sep 21 '22 06:09 maag03

@ppalaga WDYT about this, especially https://github.com/quarkiverse/quarkus-cxf/issues/248#issuecomment-982103369 (and the following two comments)?

famod avatar Jan 30 '23 23:01 famod

Before I can help, I'd need some help understanding what's the intention behind the reported use case. I also have not had time to read the whole discussion.

I can understand that @mickroll wants to use a mock of ExamplePortType in his tests. He does not want Quarkus to fail at boot with a NPE. I am rather inexperienced in using mocks. I know little about what is @InjectMock supposed to be doing. I assume the life cycle of @InjectMock-annotated fields is managed by some kind of Mockito JUnit extension (please correct me if I am wrong). But where is the behavior of the mock defined? - perhaps it will be implemented in the test method? Nevermind, if the lifecycle of the client is supposed to be managed by Mockito, then what is the point of adding the @CXFClient annotation? I'd say you can want one of them, but not both. @CXFClient will make the lifecycle of the client to be owned by Quarkus CXF extension which, by its very nature cannot inject a mock.

ppalaga avatar Jan 31 '23 13:01 ppalaga

@ppalaga For a better understanding of the use case please read my comment https://github.com/quarkiverse/quarkus-cxf/issues/248#issuecomment-869404306

I also added an example implementation of that use case as a reproducer, you can see the link right above the mentioned comment.

mickroll avatar Jan 31 '23 20:01 mickroll

Do you have any updates about this issue? Will we be able to mock @CXFClient?

matiasgonsal avatar Sep 27 '23 16:09 matiasgonsal

Closing, not needed anymore. Can't invest time, either.

mickroll avatar Jan 22 '24 13:01 mickroll