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

Cannot mock EntityManager methods with @InjectMock Session #40475

Closed
gian1200 opened this issue May 6, 2024 · 11 comments
Closed

Cannot mock EntityManager methods with @InjectMock Session #40475

gian1200 opened this issue May 6, 2024 · 11 comments
Labels
area/hibernate-orm Hibernate ORM area/testing kind/bug Something isn't working

Comments

@gian1200
Copy link
Contributor

gian1200 commented May 6, 2024

Describe the bug

It's not possible to mock all EntityManager methods with @InjectMock Session (e.g. createNativeQuery)

Related: #16437 (comment)

Expected behavior

As stated in https://quarkus.io/version/3.8/guides/hibernate-orm-panache#mocking-entitymanager-session-and-entity-instance-methods, mocking EntityManager should be possible..
Warnings/errors shouldn't appear

Actual behavior

jakarta.persistence.Query jakarta.persistence.EntityManager.createNativeQuery(String sqlString) method conflicting with org.hibernate.query.NativeQuery org.hibernate.query.QueryProducer.createNativeQuery(String sqlString) when mocking EntityManager via Session

image

Code from docs (https://quarkus.io/version/3.8/guides/hibernate-orm-panache#mocking-entitymanager-session-and-entity-instance-methods) shows some warnings for createQuery and Query, too.
image

Even when doing some manual casting with my code, I'm getting the following error

image

ERROR:

org.mockito.exceptions.misusing.WrongTypeOfReturnValue: 
Query$MockitoMock$JX88Svnf cannot be returned by createNativeQuery()
createNativeQuery() should return NativeQuery

How to Reproduce?

  1. Session extends SharedSessionContract, EntityManager,
  2. SharedSessionContract extends QueryProducer
  3. EntityManager declares jakarta.persistence.Query createQuery(String queryString), jakarta.persistence.Query createNativeQuery(String sqlString)
  4. QueryProducer declares org.hibernate.query.Query createQuery(String queryString), org.hibernate.query.NativeQuery createNativeQuery(String sqlString)
  5. When using @InjectMock Session to mock EntityManager used in PanacheRepository (getEntityManager()), QueryProducer methods are expected instead of EntityManager's

Output of uname -a or ver

No response

Output of java -version

openjdk version "17.0.11"

Quarkus version or git rev

3.8.4

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

Apache Maven 3.9.6

Additional information

  • Code Fragment:
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.hibernate.Session;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;

@QuarkusTest
class InjectionRepositoryTest {

	@InjectMock
	Session session;

	@BeforeEach
	public void setup() {
		Query mockQuery = Mockito.mock(Query.class);
		Mockito.doNothing().when(session).persist(Mockito.any());
		Mockito.when(session.createNativeQuery(Mockito.anyString())).thenReturn(mockQuery);
		Mockito.when(mockQuery.getSingleResult()).thenReturn(0l);
	}

	@Test
	void testPanacheMocking() {
		assertEquals(1, 1);
	}

}

Fails when importing org.hibernate.query.Query or jakarta.persistence.Query. Passes with org.hibernate.query.NativeQuery.

@gian1200 gian1200 added the kind/bug Something isn't working label May 6, 2024
@geoand geoand added area/hibernate-orm Hibernate ORM and removed triage/needs-triage labels May 17, 2024
Copy link

quarkus-bot bot commented May 17, 2024

/cc @gsmet (hibernate-orm), @yrodiere (hibernate-orm)

@gian1200
Copy link
Contributor Author

gian1200 commented May 17, 2024

Update:

I found a workaround to solve my issue. I don't think it's a recommended approach (AFAIK we shouldn't mock EntityManager directly) but it worked in my scenario (mock EntityManager method inside PanacheRepository).

// src/test/resources/application.properties (can't remember if these were required or not)

quarkus.arc.test.disable-application-lifecycle-observers=true
quarkus.hibernate-orm.active=false

-----------------------

// @QuarkusTest class:

EntityManager entityManager;

@InjectSpy
XXXRepository xxxRepository;

@BeforeEach
public void setup() {
	entityManager = mock(EntityManager.class);
	Query mockQuery = mock(Query.class);
	when(xxxRepository.getEntityManager()).thenReturn(entityManager);
	when(entityManager.createNativeQuery(anyString(), any(Class.class))).thenReturn(mockQuery);
}

@yrodiere
Copy link
Member

yrodiere commented May 17, 2024

Hello,

Thanks for reporting.

Do I understand correctly that you are calling Session#createNativeQuery, trying to mock it with Mockito, and surprised that Mockito requires the return value to be of the same type as Session#createNativeQuery?

I'm afraid this is just how Mockito works, nothing related to Quarkus of Hibernate ORM. If you think about it: Mockito will create a Session instance, and will have to implement the method Session#createNativeQuery, whose return type is NativeQuery. If you provide a return value that does not implement NativeQuery, how can Mockito have that method return it? It's just the wrong type.

I would suggest:

  • Simply complying with what Mockito requires -- i.e. using the type NativeQuery for mockQuery.
  • OR using @InjectMock EntityManager entityManager -- if that doesn't work, then that is a bug we need to fix, though I'm not entirely sure we can, given much of Quarkus relies on Session.

Code from docs (https://quarkus.io/version/3.8/guides/hibernate-orm-panache#mocking-entitymanager-session-and-entity-instance-methods) shows some warnings for createQuery and Query, too.
image

This warning is unrelated to mocking: the method you're trying to mock is simply deprecated.

@gian1200
Copy link
Contributor Author

gian1200 commented May 17, 2024

Hi, no. Actually, I'm trying to call/mock EntityManager methods, not Session (also EntityManager.createQuery is not deprecated.

I'll give a try to @InjectMock EntityManager entityManager. That was my first option but I remember that I read somewhere in the docs (or GitHub) that we shouldn't do that, and injectMock Session instead. If I remember correctly, I already tried it, but it didn't work (now that I think about it, probably because I get the instance from PanacheRepository.getEntityManager() instead of injecting the EntityManager; and you just gave me an idea on how I can improve my workaround with @InjectMock EntityManager).


In the docs:

Mocking EntityManager, Session and entity instance methods

If you need to mock entity instance methods, such as persist() you can do it by mocking the Hibernate ORM Session object:

From this section I understood that if I want to mock EntityManager, I should use Session (and the example comments //mocked via EntityManager mocking when injectMocking a Session ). Did I misread it?

@yrodiere
Copy link
Member

If your code uses EntityManager and @InjectMock EntityManager entityManager works, then go ahead and do that.

Otherwise, since Session extends EntityManager, and since an EntityManager in Quarkus is, in practice, just a Session in disguise, I would indeed recommend just mocking Session.

@gian1200
Copy link
Contributor Author

gian1200 commented May 17, 2024

and you just gave me an idea on how I can improve my workaround with @InjectMock EntityManager).

Replacing mock(EntityManager.class) with@InjectMock EntityManager.

Update, it didn't work.

As you just mention, it's probably injecting a Session object (which fails when colides with EntityManager methods). In this case, I'll stick to my mock(EntityManager.class) workaround, which seems to be the only way to make it work.

However, I'm not sure if it's a recommended approach, or if it should also be in someway in the docs.

org.mockito.exceptions.misusing.WrongTypeOfReturnValue: 
Query$MockitoMock$Ng11Y08m cannot be returned by createNativeQuery()
createNativeQuery() should return NativeQuery
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectSpy;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;

@QuarkusTest
class XXXRepositoryTest {

	@InjectMock
	EntityManager entityManager;

	@InjectSpy
	XXXRepository xxxRepository;

	@BeforeEach
	public void setup() {
		// entityManager = mock(EntityManager.class);
		Query mockQuery = mock(Query.class);
		when(xxxRepository.getEntityManager()).thenReturn(entityManager); // fails with or without this line
		when(entityManager.createNativeQuery(anyString(), any(Class.class))).thenReturn(mockQuery);
	}

	@Test
	void testPanacheMocking() {
		xxxRepository.methodX("", ""); //calls getEntityManager().createNativeQuery(String, XXX.class) inside
		assertEquals(1, 1);
	}

}

@yrodiere
Copy link
Member

Update, it didn't work.

Can you please share the stacktrace? As text, preferably, not a screenshot :)

From what I can see, Panache retrieves an EntityManager, not a Session, so the problem must be somewhere else.

In this case, I'll stick to my mock(EntityManager.class) workaround, which seems to be the only way to make it work.

It's not though, you can just mock the Session. As you said: "Passes with org.hibernate.query.NativeQuery."

@gian1200
Copy link
Contributor Author

gian1200 commented May 17, 2024

Can you please share the stacktrace? As text, preferably, not a screenshot :)

Sure, no problem. I'm a good person (but don't give me ideas😈 )

...
[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.

It's not though, you can just mock the Session. As you said: "Passes with org.hibernate.query.NativeQuery."

getEntityManager().createNativeQuery returns jakarta.persistence.Quary, so Query is preferred.

@yrodiere
Copy link
Member

Thank you for the stracktrace, this should help with future investigations.

It's not though, you can just mock the Session. As you said: "Passes with org.hibernate.query.NativeQuery."

getEntityManager().createNativeQuery returns jakarta.persistence.Quary, so Query is preferred.

Alright, so let's agree there are two solutions, and you prefer the one that doesn't involve a Session ;)

@gian1200
Copy link
Contributor Author

Haha, yes. It's one way of saying it.
I'd say "to use the same types on both test and production code"

@yrodiere
Copy link
Member

So to sum up, @InjectMock Session definitely isn't supposed to allow mocking EntityManager method -- just Session methods, which may override EntityManager methods. The fact that @InjectMock EntityManager doesn't work, on the other hand, seems like something that should be improved, or at least documented.

I opened #40807 to address @InjectMock EntityManager, and will close this one. Thanks for reporting and providing valuable feedback!

@yrodiere yrodiere closed this as not planned Won't fix, can't repro, duplicate, stale May 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/hibernate-orm Hibernate ORM area/testing kind/bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants