quarkusio / quarkus

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

Support for `@InjectMock EntityManager` #40807

Open yrodiere opened 6 months ago

yrodiere commented 6 months ago

Description

While @InjectMock Session works fine to mock the Hibernate ORM session, oddly @InjectMock EntityManager doesn't, even if a given test only uses EntityManager (e.g. using Panache): any method you'll mock will fail because something expect you to use the Session return type, even though you're mocking a EntityManager object (see stracktrace near the bottom).

This is a bit counter-intuitive, so we should probably look into either making @InjectMock EntityManager work correctly, or documenting the limitation and adding clear exception messages when someone tries to do @InjectMock EntityManager ("@InjectMock doesn't work with EntityManager, use Session instead").

Stacktrace from: https://github.com/quarkusio/quarkus/issues/40475#issuecomment-2117803810

...
[INFO] Running <redacted>.XXXRepositoryTest
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.619 s <<< FAILURE! -- in <redacted>.XXXRepositoryTest
[ERROR] <redacted>.XXXRepositoryTest.testPanacheMocking -- Time elapsed: 0.113 s <<< ERROR!
org.mockito.exceptions.misusing.WrongTypeOfReturnValue:

Query$MockitoMock$wgzFLNsQ cannot be returned by createNativeQuery()
createNativeQuery() should return NativeQuery
***
If you're unsure why you're getting above error read on.
Due to the nature of the syntax above problem might occur because:
1. This exception *might* occur in wrongly written multi-threaded tests.
   Please refer to Mockito FAQ on limitations of concurrency testing.
2. A spy is stubbed using when(spy.foo()).then() syntax. It is safer to stub spies -
   - with doReturn|Throw() family of methods. More in javadocs for Mockito.spy() method.

        at <redacted>.XXXRepositoryTest.setup(XXXRepositoryTest.java:32)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at io.quarkus.test.junit.QuarkusTestExtension.runExtensionMethod(QuarkusTestExtension.java:1013)
        at io.quarkus.test.junit.QuarkusTestExtension.interceptBeforeEachMethod(QuarkusTestExtension.java:808)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

2024-05-17 09:51:33,690 INFO  [io.quarkus] (main) <redacted> stopped in 0.107s
[INFO] 
[INFO] Results:
[INFO]
[ERROR] Errors: 
[ERROR]   XXXRepositoryTest.setup:32 WrongTypeOfReturnValue 
Query$MockitoMock$wgzFLNsQ cannot be returned by createNativeQuery()
createNativeQuery() should return NativeQuery
***
If you're unsure why you're getting above error read on.
Due to the nature of the syntax above problem might occur because:
1. This exception *might* occur in wrongly written multi-threaded tests.
   Please refer to Mockito FAQ on limitations of concurrency testing.
2. A spy is stubbed using when(spy.foo()).then() syntax. It is safer to stub spies - 
   - with doReturn|Throw() family of methods. More in javadocs for Mockito.spy() method.

[INFO]
[ERROR] Tests run: 14, Failures: 0, Errors: 1, Skipped: 0
...

PS: Doesn't work with doReturn(entityManager).when(solicitudRepository).getEntityManager(); neither.

Implementation ideas

No response

geoand commented 6 months ago

Oddly enough, I could not reproduce this problem in either hibernate-orm-quickstart or hibernate-orm-panache-quickstart.

For the former the test I added was:

@QuarkusTest
public class MockTest {

    @InjectMock
    EntityManager em;

    @Test
    public void test() {
        Fruit mockResponse = new Fruit();
        mockResponse.setId(1);
        mockResponse.setName("mock");
        Mockito.when(em.find(Fruit.class, 1)).thenReturn(mockResponse);

        Fruit fruit = em.find(Fruit.class, 1);
        Assertions.assertEquals("mock", fruit.getName());
    }
}

For the latter it was essentially the same thing but using FruitEntity instead of Fruit.

yrodiere commented 6 months ago

@geoand I think that in order to reproduce this, you need to mock a method whose return type is overloaded in Session. Like createNativeQuery.

geoand commented 6 months ago

Oh, you are right, adding:

        Query mock = Mockito.mock(Query.class);
        Mockito.when(em.createNativeQuery("SELECT 1 FROM FRUIT")).thenReturn(mock);

surfaces the problem.

Honestly, I don't think we can do much...

yrodiere commented 6 months ago

Yeah I don't know if we can solve this either, but we might be able to forbid @InjectMock EntityManager and provide a clear exception + document the limitation, at least.

yrodiere commented 6 months ago

Or more generally forbid @InjectMock A where we know that matching CDI beans are exposed with type B extends A (so @InjectMock should use type B).

geoand commented 6 months ago

That's probably a good idea