Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QuarkusComponentTest -> No ParameterResolver registered for parameter #41224

Closed
octopus-prime opened this issue Jun 15, 2024 · 12 comments · Fixed by #41305
Closed

QuarkusComponentTest -> No ParameterResolver registered for parameter #41224

octopus-prime opened this issue Jun 15, 2024 · 12 comments · Fixed by #41305
Labels
area/arc Issue related to ARC (dependency injection) kind/bug Something isn't working
Milestone

Comments

@octopus-prime
Copy link

octopus-prime commented Jun 15, 2024

Describe the bug

As far as i understand QuarkusComponentTest is just 'mockito-test' with other annotations.
So QuarkusComponentTest still relies on Mockito.

So let's convert a 'mockito-test' into a QuarkusComponentTest

@ExtendWith(MockitoExtension.class)
class ReadFallControllerTest {

    @InjectMocks
    ReadFallController readFallController;

    @Mock
    FallRepository fallRepository;

    @Mock
    FallMapper fallMapper;

    @Test
    void readFall(@Mock Fall fall, @Mock FallResponse fallResponse) {
        when(fallRepository.findByFallNummerOrThrow(TestData.FALLNUMMER)).thenReturn(fall);
        when(fallMapper.map(fall)).thenReturn(fallResponse);

        FallResponse result = readFallController.readFall(TestData.FALLNUMMER);

        assertThat(result, is(fallResponse));
    }
}

->

@QuarkusComponentTest
class ReadFallControllerTest {

    @Inject
    ReadFallController readFallController;

    @InjectMock
    FallRepository fallRepository;

    @InjectMock
    FallMapper fallMapper;

    @Test
    void readFall(@Mock Fall fall, @Mock FallResponse fallResponse) {
        when(fallRepository.findByFallNummerOrThrow(TestData.FALLNUMMER)).thenReturn(fall);
        when(fallMapper.map(fall)).thenReturn(fallResponse);

        FallResponse result = readFallController.readFall(TestData.FALLNUMMER);

        assertThat(result, is(fallResponse));
    }
}

Expected behavior

A fully initialized mockito - with parameter resolvers.

Actual behavior

org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter

How to Reproduce?

Run the QuarkusComponentTest

Output of uname -a or ver

No response

Output of java -version

No response

Quarkus version or git rev

No response

Build tool (ie. output of mvnw --version or gradlew --version)

No response

Additional information

Same failure with
void readFall(@InjectMock Fall fall, @InjectMock FallResponse fallResponse) {

@octopus-prime octopus-prime added the kind/bug Something isn't working label Jun 15, 2024
@geoand geoand added area/arc Issue related to ARC (dependency injection) and removed triage/needs-triage labels Jun 17, 2024
Copy link

quarkus-bot bot commented Jun 17, 2024

/cc @Ladicek (arc), @manovotn (arc), @mkouba (arc)

@mkouba
Copy link
Contributor

mkouba commented Jun 17, 2024

As far as i understand QuarkusComponentTest is just 'mockito-test' with other annotations.

@octopus-prime It is not.

So QuarkusComponentTest still relies on Mockito.

Not really, you can use QuarkusComponentTest even without Mockito and mocks in general.

The difference is that QuarkusComponentTest starts a real CDI container plus configuration service. Mocks are automatically used if a component has an unsatisfied dependency; e.g. there's a component Foo which also depends on Bar. If you test Foo in your QuarkusComponentTest but no Bar bean is available then a mock is injected instead.

Expected behavior

A fully initialized mockito - with parameter resolvers.

We don't use the MockitoExtension at all. Therefore, the @Mock parameters may not be resolved.

If you need to inject an unconfigured mock in a test method argument then replace the @Mock with @InjectMock.

@octopus-prime
Copy link
Author

@QuarkusComponentTest
class ReadFallControllerTest {

    @Inject
    ReadFallController readFallController;

    @InjectMock
    FallRepository fallRepository;

    @InjectMock
    FallMapper fallMapper;

    @Test
    void readFall(@InjectMock Fall fall, @InjectMock FallResponse fallResponse) {
        when(fallRepository.findByFallNummerOrThrow(TestData.FALLNUMMER)).thenReturn(fall);
        when(fallMapper.map(fall)).thenReturn(fallResponse);

        FallResponse result = readFallController.readFall(TestData.FALLNUMMER);

        assertThat(result, is(fallResponse));
    }
}

->

Failed to resolve parameter
No matching bean found for the type

@mkouba
Copy link
Contributor

mkouba commented Jun 18, 2024

Failed to resolve parameter
No matching bean found for the type

Could you share the full stacktrace pls?

Also it would be great if you could share a small project that would contain the "full" setup of your test.

@octopus-prime
Copy link
Author

https://github.com/octopus-prime/qct

The 'BrokenFooControllerTest' will produce

Failed to resolve parameter [com.example.Foo arg0] in method [void com.example.BrokenFooControllerTest.getFoo(com.example.Foo,com.example.FooResponse)]: No matching bean found for the type [class com.example.Foo] and qualifiers []
org.junit.jupiter.api.extension.ParameterResolutionException: Failed to resolve parameter [com.example.Foo arg0] in method [void com.example.BrokenFooControllerTest.getFoo(com.example.Foo,com.example.FooResponse)]: No matching bean found for the type [class com.example.Foo] and qualifiers []
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.IllegalStateException: No matching bean found for the type [class com.example.Foo] and qualifiers []
	at io.quarkus.test.component.QuarkusComponentTestExtension.supportsParameter(QuarkusComponentTestExtension.java:300)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:178)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)

@mkouba
Copy link
Contributor

mkouba commented Jun 18, 2024

https://github.com/octopus-prime/qct

The 'BrokenFooControllerTest' will produce

Failed to resolve parameter [com.example.Foo arg0] in method [void com.example.BrokenFooControllerTest.getFoo(com.example.Foo,com.example.FooResponse)]: No matching bean found for the type [class com.example.Foo] and qualifiers []
org.junit.jupiter.api.extension.ParameterResolutionException: Failed to resolve parameter [com.example.Foo arg0] in method [void com.example.BrokenFooControllerTest.getFoo(com.example.Foo,com.example.FooResponse)]: No matching bean found for the type [class com.example.Foo] and qualifiers []
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.IllegalStateException: No matching bean found for the type [class com.example.Foo] and qualifiers []
	at io.quarkus.test.component.QuarkusComponentTestExtension.supportsParameter(QuarkusComponentTestExtension.java:300)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:178)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)

👍 I'll take a look tomorrow.

@mkouba
Copy link
Contributor

mkouba commented Jun 19, 2024

@octopus-prime Ok, so the problem is that @InjectMock is not intended for injection of a mock of any arbitrary object. It's meant to be used for beans; see the javadoc "Instructs the test engine to inject a mock instance of a bean into the annotated field or parameter.".

Now in the reproducer, Foo and FooResponse are not beans but more like DTOs. For these objects, @org.mockito.Mock or Mockito#mock(Class) are more appropriate, i.e. something like:

    @Test
    void getFoo() {
        Foo foo = Mockito.mock(Foo.class);
        FooResponse fooResponse = Mockito.mock(FooResponse.class);
        
        when(fooRepository.findFoo("1")).thenReturn(foo);
        when(fooMapper.mapFoo(foo)).thenReturn(fooResponse);

        FooResponse result = fooController.getFoo("1");

        assertEquals(result, fooResponse);
    }

Again, for QuarkusComponentTest it's not the goal to ease the creation of mocks but create mocked beans automatically if a component under the test (a CDI bean) has an unsatisfied dependency.

I wonder if we should change the behavior but I think that it does not make sense to create a mock bean for such an object. It seems like a misuse of the API. But I agree that we should at least improve the error message so that it's more clear. If we find a way to detect this kind of misuse...

CC @manovotn

@octopus-prime
Copy link
Author

Strange... "@InjectMock is not intended for injection of a mock of any arbitrary object."... But

@QuarkusComponentTest
class UglyFooControllerTest {

    @Inject
    FooController fooController;

    @InjectMock
    FooRepository fooRepository;

    @InjectMock
    FooMapper fooMapper;

    @InjectMock
    Foo foo;

    @InjectMock
    FooResponse fooResponse;

    @Test
    void getFoo() {
        when(fooRepository.findFoo("1")).thenReturn(foo);
        when(fooMapper.mapFoo(foo)).thenReturn(fooResponse);

        FooResponse result = fooController.getFoo("1");

        assertEquals(fooResponse, result);
    }
}

Works fine.

However... both solutions need extra-lines of code :-(

@mkouba
Copy link
Contributor

mkouba commented Jun 19, 2024

Works fine.

Indeed, there's an inconsistency because @InjectMock field does contribute to the logic that is used to exclude unused beans from removal and @InjectMock parameter does not (a new bean is created for each @InjectMock but then removed if not used in a tested component). We should unify this logic. But I'm not quite sure in which way. If we want to be more correct and resource efficient then @InjectMock field should not work either. And we could make the @InjectMock parameter work but it would still be a misuse and waste of resources in the sense that a new bean is created just to create an empty mock 🤷.

However... both solutions need extra-lines of code :-(

Yes, they need. There's one more alternative:

import org.mockito.Mock;
import io.quarkus.test.component.SkipInject;

@ExtendWith(MockitoExtension.class)
@SkipInject // needed to tell the QuarkusComponentTest to skip the @Mock params
@Test
void getFoo(@Mock Foo foo, @Mock FooResponse fooResponse) {
    when(fooRepository.findFoo("1")).thenReturn(foo);
    when(fooMapper.mapFoo(foo)).thenReturn(fooResponse);
    FooResponse result = fooController.getFoo("1");
    assertEquals(result, fooResponse);
}

@manovotn
Copy link
Contributor

CC @manovotn

I think a sensible approach is to:

  • Fix the inconsistency we have in that we should also take into consideration method params with @InjectMock when marking beans as unremovable
  • Add documentation that will mention that @InjectMock isn't a universal replacement for Mockito. It has a narrow use case for mocking missing beans.

The way JUnit works, you should be able to meld together several extensions and Mockito has their own one that you can leverage for what you are trying to do. What @mkouba suggests in the last comment is IMO the "cleanest" approach you can get.

@mkouba
Copy link
Contributor

mkouba commented Jun 19, 2024

I think a sensible approach is to:

* Fix the inconsistency we have in that we should also take into consideration method params with `@InjectMock` when marking beans as unremovable

* Add documentation that will mention that `@InjectMock` isn't a universal replacement for Mockito. It has a narrow use case for mocking missing beans.

We will also skip param injection for params annotated with @org.mockito.Mock so that @SkipInject is not needed.

mkouba added a commit to mkouba/quarkus that referenced this issue Jun 19, 2024
- take into consideration method params with `@InjectMock` when marking
beans as unremovable
- document that `@InjectMock` is not intended as a universal replacement
for the functionality provided by the Mockito JUnit extension.
- also skip param injection for params annotated with
`@org.mockito.Mock` so that `@SkipInject` is not needed.
- fixes quarkusio#41224
@mkouba
Copy link
Contributor

mkouba commented Jun 19, 2024

@octopus-prime pull request sent! ;-)

@mkouba mkouba closed this as completed in 86bba7a Jun 19, 2024
@gsmet gsmet added this to the 3.12.1 milestone Jul 1, 2024
gsmet pushed a commit to gsmet/quarkus that referenced this issue Jul 1, 2024
- take into consideration method params with `@InjectMock` when marking
beans as unremovable
- document that `@InjectMock` is not intended as a universal replacement
for the functionality provided by the Mockito JUnit extension.
- also skip param injection for params annotated with
`@org.mockito.Mock` so that `@SkipInject` is not needed.
- fixes quarkusio#41224

(cherry picked from commit 86bba7a)
holly-cummins pushed a commit to holly-cummins/quarkus that referenced this issue Jul 31, 2024
- take into consideration method params with `@InjectMock` when marking
beans as unremovable
- document that `@InjectMock` is not intended as a universal replacement
for the functionality provided by the Mockito JUnit extension.
- also skip param injection for params annotated with
`@org.mockito.Mock` so that `@SkipInject` is not needed.
- fixes quarkusio#41224
danielsoro pushed a commit to danielsoro/quarkus that referenced this issue Sep 20, 2024
- take into consideration method params with `@InjectMock` when marking
beans as unremovable
- document that `@InjectMock` is not intended as a universal replacement
for the functionality provided by the Mockito JUnit extension.
- also skip param injection for params annotated with
`@org.mockito.Mock` so that `@SkipInject` is not needed.
- fixes quarkusio#41224
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/arc Issue related to ARC (dependency injection) kind/bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants