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

Can not mock non-static persist method on PanacheEntityBase #17697

Closed
edeandrea opened this issue Jun 4, 2021 · 16 comments
Closed

Can not mock non-static persist method on PanacheEntityBase #17697

edeandrea opened this issue Jun 4, 2021 · 16 comments
Labels
area/panache kind/bug Something isn't working

Comments

@edeandrea
Copy link
Contributor

edeandrea commented Jun 4, 2021

Describe the bug

In Quarkus 2.0.0.CR3 (an most likely in 1.13.x) using Panache reactive I am not able to mock the non-static methods on PanacheEntityBase (persist() for example - I haven't tried the others). Mockito does not allow you to mock them because Mockito requires an instance in Mockito.when/Mockito.doReturn(..).when.. PanacheMock does not seem to work either.

To Reproduce

I've attached a reproducer project:
panache-reactive-activerecord-persist-mock.zip

src/main/java/org/acme/rest/FruitResource.java

@Path("/fruits")
public class FruitResource {
	@POST
	@Produces(MediaType.APPLICATION_JSON)
	@Consumes(MediaType.APPLICATION_JSON)
	public Uni<Fruit> addFruit(@Valid Fruit fruit) {
		return Panache.withTransaction(() -> fruit.persist());
	}
}

src/test/java/org/acme/rest/FruitResourceTests.java

@QuarkusTest
class FruitResourceTests {
	@Test
	public void addFruit() {
		PanacheMock.mock(Fruit.class);
		PanacheMock.doReturn(Uni.createFrom().item(new Fruit(100L, "Grapefruit", "Summer fruit")))
			.when(Fruit.class).persist();

		given()
			.when()
				.contentType(ContentType.JSON)
				.body("{\"name\":\"Grapefruit\",\"description\":\"Summer fruit\"}")
				.post("/fruits")
			.then()
				.statusCode(200)
				.contentType(ContentType.JSON)
				.body(
					"id", is(100),
					"name", is("Grapefruit"),
					"description", is("Summer fruit")
				);

		PanacheMock.verify(Fruit.class).persist();
		PanacheMock.verifyNoMoreInteractions(Fruit.class);
	}
}

Running ./mvnw clean verify produces the following test failure, indicating the PanacheMock.doReturn(Uni.createFrom().item(new Fruit(100L, "Grapefruit", "Summer fruit"))).when(Fruit.class).persist(); is not actually being "picked up". The asserted id should be 100, but it isn't.

[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 18.231 s <<< FAILURE! - in org.acme.rest.FruitResourceTests
[ERROR] org.acme.rest.FruitResourceTests.addFruit  Time elapsed: 2.645 s  <<< FAILURE!
java.lang.AssertionError: 
1 expectation failed.
JSON path id doesn't match.
Expected: is <100>
  Actual: <1>

        at org.acme.rest.FruitResourceTests.addFruit(FruitResourceTests.java:30)

2021-06-04 10:29:08,142 INFO  [io.quarkus] (main) Quarkus stopped in 0.041s
[INFO] 
[INFO] Results:
[INFO] 
[ERROR] Failures: 
[ERROR]   FruitResourceTests.addFruit:30 1 expectation failed.
JSON path id doesn't match.
Expected: is <100>
  Actual: <1>

[INFO] 
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------

Environment (please complete the following information):

Output of uname -a or ver

Darwin edeandre-mac 20.4.0 Darwin Kernel Version 20.4.0: Thu Apr 22 21:46:47 PDT 2021; root:xnu-7195.101.2~1/RELEASE_X86_64 x86_64

Output of java -version

openjdk version "11.0.2" 2019-01-15
OpenJDK Runtime Environment 18.9 (build 11.0.2+9)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.2+9, mixed mode)

GraalVM version (if different from Java)

N/A

Quarkus version or git rev

2.0.0.CR3

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

Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: /Users/edeandre/.m2/wrapper/dists/apache-maven-3.6.3-bin/1iopthnavndlasol9gbrbg6bf2/apache-maven-3.6.3
Java version: 11.0.2, vendor: Oracle Corporation, runtime: /Users/edeandre/.sdkman/candidates/java/11.0.2-open
Default locale: en_US, platform encoding: UTF-8
OS name: "mac os x", version: "10.16", arch: "x86_64", family: "mac"
@edeandrea edeandrea added the kind/bug Something isn't working label Jun 4, 2021
@quarkus-bot
Copy link

quarkus-bot bot commented Jun 4, 2021

/cc @FroMage, @loicmathieu

@FroMage
Copy link
Member

FroMage commented Jun 7, 2021

To mock instance methods of an entity you need to actually mock the session:

@QuarkusTest
public class PanacheMockingTest {

    @InjectMock
    Mutiny.Session session;

    @Test
    public void testPanacheSessionMocking() {
        Person p = new Person();
        // mocked via Mutiny.Session mocking
        p.persist().await().indefinitely();
        Assertions.assertNull(p.id);

        Mockito.verify(session, Mockito.times(1)).persist(Mockito.any());
    }
}

@FroMage FroMage closed this as completed Jun 7, 2021
@edeandrea
Copy link
Contributor Author

edeandrea commented Jun 7, 2021

Where in the example though do I set up an interaction? In Panache reactive, the persist() method on the entity returns Uni<T> but Mutiny.Session.persist(Object) returns Uni<Void>?

I want to set up an interaction that is something like "When the persist() method is called, return some other Uni which the test will provide".

Mocking the Session seems very low level no? It doesn't seem intuitive to verify interactions on a session when my application code does not use the Session directly.

@loicmathieu
Copy link
Contributor

Here, the issue is that you normally should create a mocked Fruit with Mockito but you cannot as the Fruit object is created by the resteasy serialization layer.

I'm afrad there is no solution to what you want to achieve, if you test your application at the HTTP layer using restassured, you must use the restassured assertion (at the http layer). Here I would remove all the code related to mockito/panachemock.

@edeandrea
Copy link
Contributor Author

Also adding

@InjectMock
Mutiny.Session session;

doesn't work. This is what I get in the output:

[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 6.667 s <<< FAILURE! - in org.acme.rest.FruitResourceTests
[ERROR] org.acme.rest.FruitResourceTests.addFruit  Time elapsed: 0.004 s  <<< ERROR!
org.junit.jupiter.api.extension.TestInstantiationException: Failed to create test instance
Caused by: java.lang.reflect.InvocationTargetException
Caused by: org.mockito.exceptions.base.MockitoException: 
Mockito cannot mock this class: class io.quarkus.hibernate.reactive.runtime.ReactiveSessionProducer_ProducerMethod_createMutinySession_1321d110ee9e92bda147899150401e0a136779c7_ClientProxy.
Mockito can only mock non-private & non-final classes.
If you're not sure why you're getting this error, please report to the mailing list.
Java               : 11
JVM vendor name    : Oracle Corporation
JVM vendor version : 11.0.2+9
JVM name           : OpenJDK 64-Bit Server VM
JVM version        : 11.0.2+9
JVM info           : mixed mode
OS name            : Mac OS X
OS version         : 10.16
Underlying exception : java.lang.IllegalArgumentException: Unknown type: null
Caused by: java.lang.IllegalArgumentException: Unknown type: null
2021-06-07 10:04:49,594 INFO  [io.quarkus] (main) Quarkus stopped in 0.033s
[INFO] 
[INFO] Results:
[INFO] 
[ERROR] Errors: 
[ERROR]   FruitResourceTests.addFruit » TestInstantiation Failed to create test instance
[INFO] 
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0
[INFO] 

@edeandrea
Copy link
Contributor Author

I'm afrad there is no solution to what you want to achieve, if you test your application at the HTTP layer using restassured, you must use the restassured assertion (at the http layer). Here I would remove all the code related to mockito/panachemock.

I can use the rest-assured at the HTTP layer to validate the returned json is correct, but I don't need/want an entire persistence back-end just to verify my HTTP layer. I can accomplish this just fine using the repository pattern.

For the active record pattern, I'll continue with what I was doing before:

@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Uni<Fruit> addFruit(@Valid Fruit fruit) {
	return Panache.withTransaction(() ->
		Fruit.persist(fruit)
			.replaceWith(fruit)
	);
}

and the tests

@Test
public void addFruit() {
	PanacheMock.mock(Fruit.class);
	Mockito.when(Fruit.persist(Mockito.any(Fruit.class), Mockito.any()))
		.thenReturn(Uni.createFrom().voidItem());

	given()
		.when()
			.contentType(ContentType.JSON)
			.body("{\"id\":1,\"name\":\"Grapefruit\",\"description\":\"Summer fruit\"}")
			.post("/fruits")
		.then()
			.statusCode(200)
			.contentType(ContentType.JSON)
			.body(
				"id", is(1),
				"name", is("Grapefruit"),
				"description", is("Summer fruit")
			);

	PanacheMock.verify(Fruit.class).persist(Mockito.any(Fruit.class), Mockito.any());
	PanacheMock.verifyNoMoreInteractions(Fruit.class);
}

@loicmathieu
Copy link
Contributor

@edeandrea yes, this is the difference between repository where you can easily mock it, and entity. Again, here, the Fruit is created by the serialization layer and there is no way you mock the instanciation of an object JVM wide! The new is made by the serialization layer, so with entity I recommend using embedded database (h2 or testcontainer based database).

@edeandrea
Copy link
Contributor Author

edeandrea commented Jun 7, 2021

Thank you @loicmathieu & @FroMage for the conversation!

@FroMage
Copy link
Member

FroMage commented Jun 7, 2021

doesn't work. This is what I get in the output:

I have a fix for that in #17728 it's a bug.

@edeandrea
Copy link
Contributor Author

Awesome! Scanning that ticket I see

Added @ReactiveTransaction to forward to Mutiny.Session.withTransaction: method must return a Uni or be invoked from a non-VertxThread (in which case it blocks)"

Would that be an annotation I could put on a method instead of having to do something like

@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Uni<Fruit> addFruit(@Valid Fruit fruit) {
  return Panache.withTransaction(() -> this.fruitRepository.persist(fruit));
}

?

@FroMage
Copy link
Member

FroMage commented Jun 8, 2021

Yes.

@edeandrea
Copy link
Contributor Author

Nice!

Why not reuse the current @Transactional annotation rather than inventing a new one? Panache reactive already reuses the other javax.persistence annotations.

@FroMage
Copy link
Member

FroMage commented Jun 8, 2021

Because @Transactional integrates with JTA and we don't support that, ATM, so it would give you a false sense of it being the same.

@edeandrea
Copy link
Contributor Author

Got it - thanks @FroMage !

Is this going to be part of Quarkus 2.0? Or a later 2.x version? Will @TestTransaction also be made to work in tests?

@FroMage
Copy link
Member

FroMage commented Jun 8, 2021

Hopefully 2.0 yes. I don't know what @TestTransaction is.

@edeandrea
Copy link
Contributor Author

edeandrea commented Jun 8, 2021

I don't know what @TestTransaction is.

https://quarkus.io/guides/getting-started-testing#tests-and-transactions

@QuarkusTest
@TestTransaction
class FruitRepositoryTests {
	@Inject
	FruitRepository fruitRepository;

	@Test
	public void findByName() {
		this.fruitRepository.persist(new Fruit(null, "Grapefruit", "Summer fruit"));

		Optional<Fruit> fruit = this.fruitRepository.findByName("Grapefruit");
		assertThat(fruit)
			.isNotNull()
			.isPresent()
			.get()
			.extracting(Fruit::getName, Fruit::getDescription)
			.containsExactly("Grapefruit", "Summer fruit");

		assertThat(fruit.get().getId())
			.isNotNull()
			.isGreaterThan(2L);
	}
}

whereas currently in Panache reactive I have to do something like

@QuarkusTest
class FruitRepositoryTests {
	@Inject
	FruitRepository fruitRepository;

	@Test
	public void findByName() {
		Fruit fruit = Panache.getSession().withTransaction(tx -> {
                        tx.markForRollback();
			return this.fruitRepository
				.persist(new Fruit(null, "Grapefruit", "Summer fruit"))
				.replaceWith(this.fruitRepository.findByName("Grapefruit"));
		})
			.await()
			.atMost(Duration.ofSeconds(10));

		assertThat(fruit)
			.isNotNull()
			.extracting(Fruit::getName, Fruit::getDescription)
			.containsExactly("Grapefruit", "Summer fruit");

		assertThat(fruit.getId())
			.isNotNull()
			.isGreaterThan(2L);
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/panache kind/bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants