diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index 4336eae73f892..93c7195a009d2 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -427,6 +427,11 @@ quarkus-panache-common ${project.version} + + io.quarkus + quarkus-panache-mock + ${project.version} + io.quarkus quarkus-panacheql diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index a2b669527b8fb..fdaa29db155d3 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -525,6 +525,10 @@ public class MockGreetingServiceTest { ---- <1> Since we configured `greetingService` as a mock, the `GreetingResource` which uses the `GreetingService` bean, we get the mocked response instead of the response of the regular `GreetingService` bean +=== Mocking with Panache + +If you are using the `quarkus-hibernate-orm-panache` or `quarkus-mongodb-panache` extensions, check out the link:hibernate-orm-panache#mocking[Hibernate ORM with Panache Mocking] and link:mongodb-panache#mocking[MongoDB with Panache Mocking] documentation for the easiest way to mock your data access. + == Test Bootstrap Configuration Options There are a few system properties that can be used to tune the bootstrap of the test, specifically its classpath. diff --git a/docs/src/main/asciidoc/hibernate-orm-panache.adoc b/docs/src/main/asciidoc/hibernate-orm-panache.adoc index 8dcd69ff252ef..92d6492a2fab5 100644 --- a/docs/src/main/asciidoc/hibernate-orm-panache.adoc +++ b/docs/src/main/asciidoc/hibernate-orm-panache.adoc @@ -611,7 +611,7 @@ public void create(Parameter parameter){ Panache provides direct support for database locking with your entity/repository, using `findById(Object, LockModeType)` or `find().withLock(LockModeType)`. -The following examples are for the entity pattern, but the same can be used with repositories. +The following examples are for the active record pattern, but the same can be used with repositories. === First: Locking using findById(). @@ -686,6 +686,184 @@ public class PersonRepository implements PanacheRepositoryBase { } ---- +== Mocking + +=== Using the active record pattern + +If you are using the active record pattern you cannot use Mockito directly as it does not support mocking static methods, +but you can use the `quarkus-panache-mock` module which allows you to use Mockito to mock all provided static +methods, including your own. + +Add this dependency to your `pom.xml`: + +[source,xml] +---- + + io.quarkus + quarkus-panache-mock + test + +---- + + +Given this simple entity: + +[source,java] +---- +@Entity +public class Person extends PanacheEntity { + + public String name; + + public static List findOrdered() { + return find("ORDER BY name").list(); + } +} +---- + +You can write your mocking test like this: + +[source,java] +---- +@QuarkusTest +public class PanacheFunctionalityTest { + + @Test + public void testPanacheMocking() { + PanacheMock.mock(Person.class); + + // Mocked classes always return a default value + Assertions.assertEquals(0, Person.count()); + + // Now let's specify the return value + Mockito.when(Person.count()).thenReturn(23l); + Assertions.assertEquals(23, Person.count()); + + // Now let's change the return value + Mockito.when(Person.count()).thenReturn(42l); + Assertions.assertEquals(42, Person.count()); + + // Now let's call the original method + Mockito.when(Person.count()).thenCallRealMethod(); + Assertions.assertEquals(0, Person.count()); + + // Check that we called it 4 times + PanacheMock.verify(Person.class, Mockito.times(4)).count();// <1> + + // Mock only with specific parameters + Person p = new Person(); + Mockito.when(Person.findById(12l)).thenReturn(p); + Assertions.assertSame(p, Person.findById(12l)); + Assertions.assertNull(Person.findById(42l)); + + // Mock throwing + Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException()); + Assertions.assertThrows(WebApplicationException.class, () -> Person.findById(12l)); + + // We can even mock your custom methods + Mockito.when(Person.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(Person.findOrdered().isEmpty()); + + PanacheMock.verify(Person.class).findOrdered(); + PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any()); + PanacheMock.verifyNoMoreInteractions(Person.class); + } +} +---- +<1> Be sure to call your `verify` methods on `PanacheMock` rather than `Mockito`, otherwise you won't know +what mock object to pass. + +=== Using the repository pattern + +If you are using the repository pattern you can use Mockito directly, using the `quarkus-junit5-mockito` module, +which makes mocking beans much easier: + +[source,java] +---- + + io.quarkus + quarkus-junit5-mockito + test + +---- + +Given this simple entity: + +[source,java] +---- +@Entity +public class Person { + + @Id + @GeneratedValue + public Long id; + + public String name; +} +---- + +And this repository: + +[source,java] +---- +@ApplicationScoped +public class PersonRepository implements PanacheRepository { + public List findOrdered() { + return find("ORDER BY name").list(); + } +} +---- + +You can write your mocking test like this: + +[source,java] +---- +@QuarkusTest +public class PanacheFunctionalityTest { + @InjectMock + PersonRepository personRepository; + + @Test + public void testPanacheRepositoryMocking() throws Throwable { + // Mocked classes always return a default value + Assertions.assertEquals(0, personRepository.count()); + + // Now let's specify the return value + Mockito.when(personRepository.count()).thenReturn(23l); + Assertions.assertEquals(23, personRepository.count()); + + // Now let's change the return value + Mockito.when(personRepository.count()).thenReturn(42l); + Assertions.assertEquals(42, personRepository.count()); + + // Now let's call the original method + Mockito.when(personRepository.count()).thenCallRealMethod(); + Assertions.assertEquals(0, personRepository.count()); + + // Check that we called it 4 times + Mockito.verify(personRepository, Mockito.times(4)).count(); + + // Mock only with specific parameters + Person p = new Person(); + Mockito.when(personRepository.findById(12l)).thenReturn(p); + Assertions.assertSame(p, personRepository.findById(12l)); + Assertions.assertNull(personRepository.findById(42l)); + + // Mock throwing + Mockito.when(personRepository.findById(12l)).thenThrow(new WebApplicationException()); + Assertions.assertThrows(WebApplicationException.class, () -> personRepository.findById(12l)); + + Mockito.when(personRepository.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(personRepository.findOrdered().isEmpty()); + + // We can even mock your custom methods + Mockito.verify(personRepository).findOrdered(); + Mockito.verify(personRepository, Mockito.atLeastOnce()).findById(Mockito.any()); + Mockito.verifyNoMoreInteractions(personRepository); + } +} +---- + == How and why we simplify Hibernate ORM mappings When it comes to writing Hibernate ORM entities, there are a number of annoying things that users have grown used to diff --git a/docs/src/main/asciidoc/mongodb-panache.adoc b/docs/src/main/asciidoc/mongodb-panache.adoc index 474dc668c89bd..3b3f9d7add49c 100644 --- a/docs/src/main/asciidoc/mongodb-panache.adoc +++ b/docs/src/main/asciidoc/mongodb-panache.adoc @@ -867,6 +867,181 @@ public Multi streamPersons() { TIP: `@SseElementType(MediaType.APPLICATION_JSON)` tells RESTEasy to serialize the object in JSON. +== Mocking + +=== Using the active-record pattern + +If you are using the active-record pattern you cannot use Mockito directly as it does not support mocking static methods, +but you can use the `quarkus-panache-mock` module which allows you to use Mockito to mock all provided static +methods, including your own. + +Add this dependency to your `pom.xml`: + +[source,xml] +---- + + io.quarkus + quarkus-panache-mock + test + +---- + +Given this simple entity: + +[source,java] +---- +public class Person extends PanacheMongoEntity { + + public String name; + + public static List findOrdered() { + return findAll(Sort.by("lastname", "firstname")).list(); + } +} +---- + +You can write your mocking test like this: + +[source,java] +---- +@QuarkusTest +public class PanacheFunctionalityTest { + + @Test + public void testPanacheMocking() { + PanacheMock.mock(Person.class); + + // Mocked classes always return a default value + Assertions.assertEquals(0, Person.count()); + + // Now let's specify the return value + Mockito.when(Person.count()).thenReturn(23l); + Assertions.assertEquals(23, Person.count()); + + // Now let's change the return value + Mockito.when(Person.count()).thenReturn(42l); + Assertions.assertEquals(42, Person.count()); + + // Now let's call the original method + Mockito.when(Person.count()).thenCallRealMethod(); + Assertions.assertEquals(0, Person.count()); + + // Check that we called it 4 times + PanacheMock.verify(Person.class, Mockito.times(4)).count();// <1> + + // Mock only with specific parameters + Person p = new Person(); + Mockito.when(Person.findById(12l)).thenReturn(p); + Assertions.assertSame(p, Person.findById(12l)); + Assertions.assertNull(Person.findById(42l)); + + // Mock throwing + Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException()); + Assertions.assertThrows(WebApplicationException.class, () -> Person.findById(12l)); + + // We can even mock your custom methods + Mockito.when(Person.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(Person.findOrdered().isEmpty()); + + PanacheMock.verify(Person.class).findOrdered(); + PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any()); + PanacheMock.verifyNoMoreInteractions(Person.class); + } +} +---- +<1> Be sure to call your `verify` methods on `PanacheMock` rather than `Mockito`, otherwise you won't know +what mock object to pass. + +=== Using the repository pattern + +If you are using the repository pattern you can use Mockito directly, using the `quarkus-junit5-mockito` module, +which makes mocking beans much easier: + +[source,java] +---- + + io.quarkus + quarkus-junit5-mockito + test + +---- + +Given this simple entity: + +[source,java] +---- +public class Person { + + @BsonId + public Long id; + + public String name; +} +---- + +And this repository: + +[source,java] +---- +@ApplicationScoped +public class PersonRepository implements PanacheMongoRepository { + public List findOrdered() { + return findAll(Sort.by("lastname", "firstname")).list(); + } +} +---- + +You can write your mocking test like this: + +[source,java] +---- +@QuarkusTest +public class PanacheFunctionalityTest { + @InjectMock + PersonRepository personRepository; + + @Test + public void testPanacheRepositoryMocking() throws Throwable { + // Mocked classes always return a default value + Assertions.assertEquals(0, personRepository.count()); + + // Now let's specify the return value + Mockito.when(personRepository.count()).thenReturn(23l); + Assertions.assertEquals(23, personRepository.count()); + + // Now let's change the return value + Mockito.when(personRepository.count()).thenReturn(42l); + Assertions.assertEquals(42, personRepository.count()); + + // Now let's call the original method + Mockito.when(personRepository.count()).thenCallRealMethod(); + Assertions.assertEquals(0, personRepository.count()); + + // Check that we called it 4 times + Mockito.verify(personRepository, Mockito.times(4)).count(); + + // Mock only with specific parameters + Person p = new Person(); + Mockito.when(personRepository.findById(12l)).thenReturn(p); + Assertions.assertSame(p, personRepository.findById(12l)); + Assertions.assertNull(personRepository.findById(42l)); + + // Mock throwing + Mockito.when(personRepository.findById(12l)).thenThrow(new WebApplicationException()); + Assertions.assertThrows(WebApplicationException.class, () -> personRepository.findById(12l)); + + Mockito.when(personRepository.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(personRepository.findOrdered().isEmpty()); + + // We can even mock your custom methods + Mockito.verify(personRepository).findOrdered(); + Mockito.verify(personRepository, Mockito.atLeastOnce()).findById(Mockito.any()); + Mockito.verifyNoMoreInteractions(personRepository); + } +} +---- + + == How and why we simplify MongoDB API When it comes to writing MongoDB entities, there are a number of annoying things that users have grown used to diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaEntityEnhancer.java b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaEntityEnhancer.java index f29c14ad090c2..7e3135519826c 100644 --- a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaEntityEnhancer.java +++ b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheJpaEntityEnhancer.java @@ -1,6 +1,7 @@ package io.quarkus.hibernate.orm.panache.deployment; import java.lang.reflect.Modifier; +import java.util.List; import javax.persistence.Transient; @@ -22,6 +23,7 @@ import io.quarkus.panache.common.deployment.EntityModel; import io.quarkus.panache.common.deployment.MetamodelInfo; import io.quarkus.panache.common.deployment.PanacheEntityEnhancer; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizer; public class PanacheJpaEntityEnhancer extends PanacheEntityEnhancer>> { @@ -38,15 +40,15 @@ public class PanacheJpaEntityEnhancer extends PanacheEntityEnhancer methodCustomizers) { + super(index, PanacheResourceProcessor.DOTNAME_PANACHE_ENTITY_BASE, methodCustomizers); modelInfo = new MetamodelInfo<>(); } @Override public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) { return new PanacheJpaEntityClassVisitor(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, - indexView.getClassByName(DotName.createSimple(className))); + indexView.getClassByName(DotName.createSimple(className)), methodCustomizers); } static class PanacheJpaEntityClassVisitor extends PanacheEntityClassVisitor { @@ -54,8 +56,9 @@ static class PanacheJpaEntityClassVisitor extends PanacheEntityClassVisitor> modelInfo, ClassInfo panacheEntityBaseClassInfo, - ClassInfo entityInfo) { - super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo); + ClassInfo entityInfo, + List methodCustomizers) { + super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo, methodCustomizers); } @Override diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java index 7de5c41758178..3d64066db93f5 100644 --- a/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java +++ b/extensions/panache/hibernate-orm-panache/deployment/src/main/java/io/quarkus/hibernate/orm/panache/deployment/PanacheResourceProcessor.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.persistence.EntityManager; import javax.persistence.NamedQueries; @@ -38,6 +39,8 @@ import io.quarkus.panache.common.deployment.MetamodelInfo; import io.quarkus.panache.common.deployment.PanacheEntityClassesBuildItem; import io.quarkus.panache.common.deployment.PanacheFieldAccessEnhancer; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizer; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizerBuildItem; import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer; public final class PanacheResourceProcessor { @@ -76,7 +79,11 @@ void build(CombinedIndexBuildItem index, BuildProducer transformers, HibernateEnhancersRegisteredBuildItem hibernateMarker, BuildProducer entityClasses, - BuildProducer namedQueries) throws Exception { + BuildProducer namedQueries, + List methodCustomizersBuildItems) throws Exception { + + List methodCustomizers = methodCustomizersBuildItems.stream() + .map(bi -> bi.getMethodCustomizer()).collect(Collectors.toList()); PanacheJpaRepositoryEnhancer daoEnhancer = new PanacheJpaRepositoryEnhancer(index.getIndex()); Set daoClasses = new HashSet<>(); @@ -107,7 +114,7 @@ void build(CombinedIndexBuildItem index, namedQueries.produce(new NamedQueryEntityClassBuildStep(parameterType.name().toString(), typeNamedQueries)); } - PanacheJpaEntityEnhancer modelEnhancer = new PanacheJpaEntityEnhancer(index.getIndex()); + PanacheJpaEntityEnhancer modelEnhancer = new PanacheJpaEntityEnhancer(index.getIndex(), methodCustomizers); Set modelClasses = new HashSet<>(); // Note that we do this in two passes because for some reason Jandex does not give us subtypes // of PanacheEntity if we ask for subtypes of PanacheEntityBase diff --git a/extensions/panache/hibernate-orm-panache/runtime/pom.xml b/extensions/panache/hibernate-orm-panache/runtime/pom.xml index 619864a3a1296..db189f7b4f32d 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/pom.xml +++ b/extensions/panache/hibernate-orm-panache/runtime/pom.xml @@ -39,6 +39,16 @@ quarkus-jackson true + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + org.hibernate diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java index e786ff47d73e9..45b64eb2f4ee7 100644 --- a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java +++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java @@ -2,6 +2,7 @@ import java.lang.reflect.Modifier; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.bson.codecs.pojo.annotations.BsonIgnore; @@ -19,6 +20,7 @@ import io.quarkus.panache.common.deployment.EntityModel; import io.quarkus.panache.common.deployment.MetamodelInfo; import io.quarkus.panache.common.deployment.PanacheEntityEnhancer; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizer; public class PanacheMongoEntityEnhancer extends PanacheEntityEnhancer>> { public final static String MONGO_OPERATIONS_NAME = MongoOperations.class.getName(); @@ -28,23 +30,23 @@ public class PanacheMongoEntityEnhancer extends PanacheEntityEnhancer entities = new HashMap<>(); - public PanacheMongoEntityEnhancer(IndexView index) { - super(index, PanacheResourceProcessor.DOTNAME_PANACHE_ENTITY_BASE); + public PanacheMongoEntityEnhancer(IndexView index, List methodCustomizers) { + super(index, PanacheResourceProcessor.DOTNAME_PANACHE_ENTITY_BASE, methodCustomizers); modelInfo = new MetamodelInfo<>(); } @Override public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) { return new PanacheMongoEntityClassVisitor(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, - indexView.getClassByName(DotName.createSimple(className))); + indexView.getClassByName(DotName.createSimple(className)), methodCustomizers); } static class PanacheMongoEntityClassVisitor extends PanacheEntityClassVisitor { public PanacheMongoEntityClassVisitor(String className, ClassVisitor outputClassVisitor, MetamodelInfo> modelInfo, ClassInfo panacheEntityBaseClassInfo, - ClassInfo entityInfo) { - super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo); + ClassInfo entityInfo, List methodCustomizers) { + super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo, methodCustomizers); } @Override diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java index 96ae8cdf948f6..25bfd54ddcb43 100644 --- a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java +++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import org.bson.codecs.pojo.annotations.BsonProperty; import org.bson.types.ObjectId; @@ -51,6 +52,8 @@ import io.quarkus.mongodb.panache.reactive.ReactivePanacheMongoRepositoryBase; import io.quarkus.panache.common.deployment.PanacheEntityClassesBuildItem; import io.quarkus.panache.common.deployment.PanacheFieldAccessEnhancer; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizer; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizerBuildItem; import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer; public class PanacheResourceProcessor { @@ -133,7 +136,11 @@ void buildImperative(CombinedIndexBuildItem index, BuildProducer transformers, BuildProducer reflectiveClass, BuildProducer propertyMappingClass, - BuildProducer entityClasses) { + BuildProducer entityClasses, + List methodCustomizersBuildItems) { + + List methodCustomizers = methodCustomizersBuildItems.stream() + .map(bi -> bi.getMethodCustomizer()).collect(Collectors.toList()); PanacheMongoRepositoryEnhancer daoEnhancer = new PanacheMongoRepositoryEnhancer(index.getIndex()); Set daoClasses = new HashSet<>(); @@ -167,7 +174,7 @@ void buildImperative(CombinedIndexBuildItem index, propertyMappingClass.produce(new PropertyMappingClassBuildStep(parameterType.name().toString())); } - PanacheMongoEntityEnhancer modelEnhancer = new PanacheMongoEntityEnhancer(index.getIndex()); + PanacheMongoEntityEnhancer modelEnhancer = new PanacheMongoEntityEnhancer(index.getIndex(), methodCustomizers); Set modelClasses = new HashSet<>(); // Note that we do this in two passes because for some reason Jandex does not give us subtypes // of PanacheMongoEntity if we ask for subtypes of PanacheMongoEntityBase @@ -214,7 +221,11 @@ void buildMutiny(CombinedIndexBuildItem index, ApplicationIndexBuildItem applicationIndex, BuildProducer reflectiveClass, BuildProducer propertyMappingClass, - BuildProducer transformers) { + BuildProducer transformers, + List methodCustomizersBuildItems) { + + List methodCustomizers = methodCustomizersBuildItems.stream() + .map(bi -> bi.getMethodCustomizer()).collect(Collectors.toList()); ReactivePanacheMongoRepositoryEnhancer daoEnhancer = new ReactivePanacheMongoRepositoryEnhancer(index.getIndex()); Set daoClasses = new HashSet<>(); @@ -249,7 +260,8 @@ void buildMutiny(CombinedIndexBuildItem index, propertyMappingClass.produce(new PropertyMappingClassBuildStep(parameterType.name().toString())); } - ReactivePanacheMongoEntityEnhancer modelEnhancer = new ReactivePanacheMongoEntityEnhancer(index.getIndex()); + ReactivePanacheMongoEntityEnhancer modelEnhancer = new ReactivePanacheMongoEntityEnhancer(index.getIndex(), + methodCustomizers); Set modelClasses = new HashSet<>(); // Note that we do this in two passes because for some reason Jandex does not give us subtypes // of PanacheMongoEntity if we ask for subtypes of PanacheMongoEntityBase diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ReactivePanacheMongoEntityEnhancer.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ReactivePanacheMongoEntityEnhancer.java index f204029cdd645..679d8cb9794ce 100644 --- a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ReactivePanacheMongoEntityEnhancer.java +++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/ReactivePanacheMongoEntityEnhancer.java @@ -2,6 +2,7 @@ import java.lang.reflect.Modifier; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.bson.codecs.pojo.annotations.BsonIgnore; @@ -19,6 +20,7 @@ import io.quarkus.panache.common.deployment.EntityModel; import io.quarkus.panache.common.deployment.MetamodelInfo; import io.quarkus.panache.common.deployment.PanacheEntityEnhancer; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizer; public class ReactivePanacheMongoEntityEnhancer extends PanacheEntityEnhancer>> { public final static String MONGO_OPERATIONS_NAME = ReactiveMongoOperations.class.getName(); @@ -28,23 +30,23 @@ public class ReactivePanacheMongoEntityEnhancer extends PanacheEntityEnhancer entities = new HashMap<>(); - public ReactivePanacheMongoEntityEnhancer(IndexView index) { - super(index, PanacheResourceProcessor.DOTNAME_MUTINY_PANACHE_ENTITY_BASE); + public ReactivePanacheMongoEntityEnhancer(IndexView index, List methodCustomizers) { + super(index, PanacheResourceProcessor.DOTNAME_MUTINY_PANACHE_ENTITY_BASE, methodCustomizers); modelInfo = new MetamodelInfo<>(); } @Override public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) { return new PanacheMongoEntityClassVisitor(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, - indexView.getClassByName(DotName.createSimple(className))); + indexView.getClassByName(DotName.createSimple(className)), methodCustomizers); } static class PanacheMongoEntityClassVisitor extends PanacheEntityClassVisitor { public PanacheMongoEntityClassVisitor(String className, ClassVisitor outputClassVisitor, MetamodelInfo> modelInfo, ClassInfo panacheEntityBaseClassInfo, - ClassInfo entityInfo) { - super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo); + ClassInfo entityInfo, List methodCustomizers) { + super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo, entityInfo, methodCustomizers); } @Override diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/JandexUtil.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/JandexUtil.java index 42bab758fae6d..475e1990dc9ba 100644 --- a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/JandexUtil.java +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/JandexUtil.java @@ -426,8 +426,6 @@ public static Type[] getParameterTypes(String methodDescriptor) { } return args.toArray(new Type[0]); } -<<<<<<< HEAD -======= public static int getParameterSize(Type paramType) { if (paramType.kind() == Kind.PRIMITIVE) { @@ -439,5 +437,4 @@ public static int getParameterSize(Type paramType) { } return 1; } ->>>>>>> d0599c8c73... fixup enhancers } diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java index 7558c726a474b..e8f5f1bf9a1f9 100644 --- a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheEntityEnhancer.java @@ -1,5 +1,7 @@ package io.quarkus.panache.common.deployment; +import java.lang.reflect.Modifier; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -45,10 +47,13 @@ public abstract class PanacheEntityEnhancer methodCustomizers; - public PanacheEntityEnhancer(IndexView index, DotName panacheEntityBaseName) { + public PanacheEntityEnhancer(IndexView index, DotName panacheEntityBaseName, + List methodCustomizers) { this.panacheEntityBaseClassInfo = index.getClassByName(panacheEntityBaseName); this.indexView = index; + this.methodCustomizers = methodCustomizers; } @Override @@ -63,11 +68,13 @@ protected abstract static class PanacheEntityClassVisitor modelInfo; private ClassInfo panacheEntityBaseClassInfo; protected ClassInfo entityInfo; + protected List methodCustomizers; public PanacheEntityClassVisitor(String className, ClassVisitor outputClassVisitor, MetamodelInfo> modelInfo, ClassInfo panacheEntityBaseClassInfo, - ClassInfo entityInfo) { + ClassInfo entityInfo, + List methodCustomizers) { super(Gizmo.ASM_API_VERSION, outputClassVisitor); thisClass = Type.getType("L" + className.replace('.', '/') + ";"); this.modelInfo = modelInfo; @@ -75,6 +82,7 @@ public PanacheEntityClassVisitor(String className, ClassVisitor outputClassVisit fields = entityModel != null ? entityModel.fields : null; this.panacheEntityBaseClassInfo = panacheEntityBaseClassInfo; this.entityInfo = entityInfo; + this.methodCustomizers = methodCustomizers; } @Override @@ -120,6 +128,19 @@ public MethodVisitor visitMethod(int access, String methodName, String descripto String[] exceptions) { userMethods.add(methodName + "/" + descriptor); MethodVisitor superVisitor = super.visitMethod(access, methodName, descriptor, signature, exceptions); + if (Modifier.isStatic(access) + && Modifier.isPublic(access) + && (access & Opcodes.ACC_SYNTHETIC) == 0 + && !methodCustomizers.isEmpty()) { + org.jboss.jandex.Type[] argTypes = JandexUtil.getParameterTypes(descriptor); + MethodInfo method = this.entityInfo.method(methodName, argTypes); + if (method == null) { + throw new IllegalStateException( + "Could not find indexed method: " + thisClass + "." + methodName + " with descriptor " + descriptor + + " and arg types " + Arrays.toString(argTypes)); + } + superVisitor = new PanacheMethodCustomizerVisitor(superVisitor, method, thisClass, methodCustomizers); + } return new PanacheFieldAccessMethodVisitor(superVisitor, thisClass.getInternalName(), methodName, descriptor, modelInfo); } @@ -162,6 +183,9 @@ private void generateMethod(MethodInfo method, AnnotationValue targetReturnTypeE mv.visitParameter(method.parameterName(i), 0 /* modifiers */); } mv.visitCode(); + for (PanacheMethodCustomizer customizer : methodCustomizers) { + customizer.customize(thisClass, method, mv); + } // inject model injectModel(mv); for (int i = 0; i < parameters.size(); i++) { diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizer.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizer.java new file mode 100644 index 0000000000000..bf9a86b488bf8 --- /dev/null +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizer.java @@ -0,0 +1,11 @@ +package io.quarkus.panache.common.deployment; + +import org.jboss.jandex.MethodInfo; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +public interface PanacheMethodCustomizer { + + public void customize(Type entityClassSignature, MethodInfo method, MethodVisitor mv); + +} diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerBuildItem.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerBuildItem.java new file mode 100644 index 0000000000000..293d6cf78669a --- /dev/null +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerBuildItem.java @@ -0,0 +1,18 @@ +package io.quarkus.panache.common.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Build item to declare that a {@link PanacheMethodCustomizer} should be used on Panache-enhanced methods. + */ +public final class PanacheMethodCustomizerBuildItem extends MultiBuildItem { + private PanacheMethodCustomizer methodCustomizer; + + public PanacheMethodCustomizerBuildItem(PanacheMethodCustomizer methodCustomizer) { + this.methodCustomizer = methodCustomizer; + } + + public PanacheMethodCustomizer getMethodCustomizer() { + return methodCustomizer; + } +} diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerVisitor.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerVisitor.java new file mode 100644 index 0000000000000..4fe968a6b3a53 --- /dev/null +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheMethodCustomizerVisitor.java @@ -0,0 +1,32 @@ +package io.quarkus.panache.common.deployment; + +import java.util.List; + +import org.jboss.jandex.MethodInfo; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import io.quarkus.gizmo.Gizmo; + +public class PanacheMethodCustomizerVisitor extends MethodVisitor { + + private List methodCustomizers; + private MethodInfo method; + private Type thisClass; + + public PanacheMethodCustomizerVisitor(MethodVisitor superVisitor, MethodInfo method, Type thisClass, + List methodCustomizers) { + super(Gizmo.ASM_API_VERSION, superVisitor); + this.thisClass = thisClass; + this.method = method; + this.methodCustomizers = methodCustomizers; + } + + @Override + public void visitCode() { + super.visitCode(); + for (PanacheMethodCustomizer customizer : methodCustomizers) { + customizer.customize(thisClass, method, mv); + } + } +} diff --git a/extensions/panache/panache-mock/pom.xml b/extensions/panache/panache-mock/pom.xml new file mode 100644 index 0000000000000..df5f35d23e27d --- /dev/null +++ b/extensions/panache/panache-mock/pom.xml @@ -0,0 +1,37 @@ + + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../../build-parent/pom.xml + + 4.0.0 + + quarkus-panache-mock + Quarkus - Panache - Mock + Mocking with Panache + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-panache-common-deployment + + + io.quarkus + quarkus-junit5-mockito + + + org.mockito + mockito-core + + + diff --git a/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/PanacheMock.java b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/PanacheMock.java new file mode 100644 index 0000000000000..1c9748ef5ff27 --- /dev/null +++ b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/PanacheMock.java @@ -0,0 +1,110 @@ +package io.quarkus.panache.mock; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.mockito.Mockito; +import org.mockito.internal.debugging.LocationImpl; +import org.mockito.internal.invocation.DefaultInvocationFactory; +import org.mockito.internal.invocation.InterceptedInvocation; +import org.mockito.internal.invocation.RealMethod; +import org.mockito.internal.util.MockUtil; +import org.mockito.invocation.MockHandler; +import org.mockito.mock.MockCreationSettings; +import org.mockito.verification.VerificationMode; + +public class PanacheMock { + + public static volatile boolean IsMockEnabled = false; + + private final static Map, Object> mocks = new ConcurrentHashMap<>(); + + @SuppressWarnings("unchecked") + public static T getMock(Class klass) { + return (T) mocks.get(klass); + } + + public static Object[] getMocks(Class... classes) { + Object[] mocks = new Object[classes.length]; + for (int i = 0; i < classes.length; i++) { + mocks[i] = getMock(classes[i]); + } + return mocks; + } + + public static void mock(Class... classes) { + for (Class klass : classes) { + mocks.computeIfAbsent(klass, v -> Mockito.mock(klass)); + } + IsMockEnabled = !mocks.isEmpty(); + } + + public static void reset() { + mocks.clear(); + IsMockEnabled = false; + } + + public static boolean isMocked(Class klass) { + return mocks.containsKey(klass); + } + + public static Object mockMethod(Class klass, String methodName, Class[] parameterTypes, Object[] args) + throws InvokeRealMethodException { + try { + Method invokedMethod = klass.getDeclaredMethod(methodName, parameterTypes); + Object mock = getMock(klass); + MockCreationSettings settings = MockUtil.getMockSettings(mock); + MyRealMethod myRealMethod = new MyRealMethod(); + InterceptedInvocation invocation = DefaultInvocationFactory.createInvocation(mock, invokedMethod, args, + myRealMethod, settings, new LocationImpl(new Throwable(), true)); + MockHandler handler = MockUtil.getMockHandler(mock); + return handler.handle(invocation); + } catch (InvokeRealMethodException e) { + throw e; + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + // + // Delegating + + public static T verify(Class klass) { + return Mockito.verify(getMock(klass)); + } + + public static T verify(Class klass, VerificationMode verificationMode) { + return Mockito.verify(getMock(klass), verificationMode); + } + + public static void verifyNoMoreInteractions(Class... classes) { + Mockito.verifyNoMoreInteractions(getMocks(classes)); + } + + public static void verifyNoInteractions(Class... classes) { + Mockito.verifyNoInteractions(getMocks(classes)); + } + + @SuppressWarnings("serial") + public static class InvokeRealMethodException extends Exception { + } + + @SuppressWarnings("serial") + public static class MyRealMethod implements RealMethod { + + @Override + public boolean isInvokable() { + return true; + } + + @Override + public Object invoke() throws Throwable { + throw new InvokeRealMethodException(); + } + + } + +} diff --git a/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockAfterEachTest.java b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockAfterEachTest.java new file mode 100644 index 0000000000000..53a3b60cf959f --- /dev/null +++ b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockAfterEachTest.java @@ -0,0 +1,13 @@ +package io.quarkus.panache.mock.impl; + +import io.quarkus.panache.mock.PanacheMock; +import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback; + +public class PanacheMockAfterEachTest implements QuarkusTestAfterEachCallback { + + @Override + public void afterEach(Object testInstance) { + PanacheMock.reset(); + } + +} diff --git a/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockBuildChainCustomizer.java b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockBuildChainCustomizer.java new file mode 100644 index 0000000000000..46334e1b6555b --- /dev/null +++ b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockBuildChainCustomizer.java @@ -0,0 +1,37 @@ +package io.quarkus.panache.mock.impl; + +import java.util.function.Consumer; + +import org.jboss.jandex.Index; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizerBuildItem; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer; + +public final class PanacheMockBuildChainCustomizer implements TestBuildChainCustomizerProducer { + + @Override + public Consumer produce(Index testClassesIndex) { + return new Consumer() { + + @Override + public void accept(BuildChainBuilder buildChainBuilder) { + buildChainBuilder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + LaunchModeBuildItem launchMode = context.consume(LaunchModeBuildItem.class); + if (launchMode.getLaunchMode() == LaunchMode.TEST) { + context.produce(new PanacheMethodCustomizerBuildItem(new PanacheMockMethodCustomizer())); + } + } + }).produces(PanacheMethodCustomizerBuildItem.class) + .consumes(LaunchModeBuildItem.class) + .build(); + } + }; + } +} diff --git a/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockMethodCustomizer.java b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockMethodCustomizer.java new file mode 100644 index 0000000000000..00463e6a58df6 --- /dev/null +++ b/extensions/panache/panache-mock/src/main/java/io/quarkus/panache/mock/impl/PanacheMockMethodCustomizer.java @@ -0,0 +1,125 @@ +package io.quarkus.panache.mock.impl; + +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type.Kind; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import io.quarkus.panache.common.deployment.JandexUtil; +import io.quarkus.panache.common.deployment.PanacheMethodCustomizer; +import io.quarkus.panache.mock.PanacheMock; + +public class PanacheMockMethodCustomizer implements PanacheMethodCustomizer { + + private final static String PANACHE_MOCK_BINARY_NAME = PanacheMock.class.getName().replace('.', '/'); + private final static String PANACHE_MOCK_INVOKE_REAL_METHOD_EXCEPTION_BINARY_NAME = PanacheMock.InvokeRealMethodException.class + .getName().replace('.', '/'); + + @Override + public void customize(Type entityClassSignature, MethodInfo method, MethodVisitor mv) { + /* + * Generated code: + * + * if(PanacheMock.IsMockEnabled && PanacheMock.isMocked(TestClass.class)) { + * try { + * return (int)PanacheMock.mockMethod(TestClass.class, "foo", new Class[] {int.class}, new Object[] {arg}); + * } catch (PanacheMock.InvokeRealMethodException e) { + * // fall-through + * } + * } + * + * Bytecode approx: + * + * 0: getstatic #16 // Field PanacheMock.IsMockEnabled:Z + * 3: ifeq 50 + * 6: ldc #1 // class MyTestMockito$TestClass + * 8: invokestatic #22 // Method PanacheMock.isMocked:(Ljava/lang/Class;)Z + * 11: ifeq 50 + * 14: ldc #1 // class MyTestMockito$TestClass + * 16: ldc #26 // String foo + * + * 18: iconst_1 + * 19: anewarray #27 // class java/lang/Class + * 22: dup + * 23: iconst_0 + * 24: getstatic #29 // Field java/lang/Integer.TYPE:Ljava/lang/Class; + * 27: aastore + * + * 28: iconst_1 + * 29: anewarray #3 // class java/lang/Object + * 32: dup + * 33: iconst_0 + * 34: iload_0 + * 35: invokestatic #35 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; + * 38: aastore + * + * 39: invokestatic #39 // Method + * PanacheMock.mockMethod:(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/Object;)Ljava/lang/Object; + * 42: checkcast #30 // class java/lang/Integer + * 45: invokevirtual #43 // Method java/lang/Integer.intValue:()I + * 48: ireturn + * 49: astore_1 + */ + Label realMethodLabel = new Label(); + + mv.visitFieldInsn(Opcodes.GETSTATIC, PANACHE_MOCK_BINARY_NAME, "IsMockEnabled", "Z"); + mv.visitJumpInsn(Opcodes.IFEQ, realMethodLabel); + + mv.visitLdcInsn(entityClassSignature); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, PANACHE_MOCK_BINARY_NAME, "isMocked", "(Ljava/lang/Class;)Z", false); + mv.visitJumpInsn(Opcodes.IFEQ, realMethodLabel); + + Label tryStart = new Label(); + Label tryEnd = new Label(); + Label tryHandler = new Label(); + mv.visitTryCatchBlock(tryStart, tryEnd, tryHandler, PANACHE_MOCK_INVOKE_REAL_METHOD_EXCEPTION_BINARY_NAME); + mv.visitLabel(tryStart); + + mv.visitLdcInsn(entityClassSignature); + mv.visitLdcInsn(method.name()); + + mv.visitLdcInsn(method.parameters().size()); + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Class"); + + int i = 0; + for (org.jboss.jandex.Type paramType : method.parameters()) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(i); + JandexUtil.visitLdc(mv, paramType); + mv.visitInsn(Opcodes.AASTORE); + i++; + } + + mv.visitLdcInsn(method.parameters().size()); + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object"); + + i = 0; + int paramSlot = 0; + for (org.jboss.jandex.Type paramType : method.parameters()) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(i); + mv.visitVarInsn(JandexUtil.getLoadOpcode(paramType), paramSlot); + JandexUtil.boxIfRequired(mv, paramType); + mv.visitInsn(Opcodes.AASTORE); + i++; + paramSlot += JandexUtil.getParameterSize(paramType); + } + + mv.visitMethodInsn(Opcodes.INVOKESTATIC, PANACHE_MOCK_BINARY_NAME, "mockMethod", + "(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/Object;)Ljava/lang/Object;", false); + JandexUtil.unboxIfRequired(mv, method.returnType()); + if (method.returnType().kind() != Kind.PRIMITIVE) { + mv.visitTypeInsn(Opcodes.CHECKCAST, method.returnType().name().toString('/')); + } + + mv.visitInsn(JandexUtil.getReturnInstruction(method.returnType())); + + mv.visitLabel(tryHandler); + mv.visitInsn(Opcodes.POP); + mv.visitLabel(tryEnd); + + mv.visitLabel(realMethodLabel); + } +} diff --git a/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer b/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer new file mode 100644 index 0000000000000..8e2b635300aac --- /dev/null +++ b/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer @@ -0,0 +1 @@ +io.quarkus.panache.mock.impl.PanacheMockBuildChainCustomizer diff --git a/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback b/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback new file mode 100644 index 0000000000000..fc04ccca8c2d8 --- /dev/null +++ b/extensions/panache/panache-mock/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback @@ -0,0 +1 @@ +io.quarkus.panache.mock.impl.PanacheMockAfterEachTest diff --git a/extensions/panache/pom.xml b/extensions/panache/pom.xml index 34220535ae042..8a36507c66f7f 100644 --- a/extensions/panache/pom.xml +++ b/extensions/panache/pom.xml @@ -15,6 +15,7 @@ pom panache-common + panache-mock hibernate-orm-panache mongodb-panache panacheql diff --git a/integration-tests/hibernate-orm-panache/pom.xml b/integration-tests/hibernate-orm-panache/pom.xml index 9006bfdc8c368..df67b02643d5c 100644 --- a/integration-tests/hibernate-orm-panache/pom.xml +++ b/integration-tests/hibernate-orm-panache/pom.xml @@ -78,6 +78,17 @@ quarkus-jackson test + + io.quarkus + quarkus-panache-mock + test + + + net.bytebuddy + byte-buddy + + + com.fasterxml.jackson.module jackson-module-jaxb-annotations diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/MockablePersonRepository.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/MockablePersonRepository.java new file mode 100644 index 0000000000000..a83ca6dcecaa1 --- /dev/null +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/MockablePersonRepository.java @@ -0,0 +1,14 @@ +package io.quarkus.it.panache; + +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.hibernate.orm.panache.PanacheRepository; + +@ApplicationScoped +public class MockablePersonRepository implements PanacheRepository { + public List findOrdered() { + return find("ORDER BY name").list(); + } +} diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Person.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Person.java index 8042ebc531645..5afa058e8dbb1 100644 --- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Person.java +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Person.java @@ -41,7 +41,7 @@ public class Person extends PanacheEntity { @Transient public int serialisationTrick; - public static List findOrdered() { + public static List findOrdered() { return find("ORDER BY name").list(); } @@ -55,4 +55,8 @@ public int getSerialisationTrick() { public void setSerialisationTrick(int serialisationTrick) { this.serialisationTrick = serialisationTrick; } + + public static long methodWithPrimitiveParams(boolean b, byte bb, short s, int i, long l, float f, double d, char c) { + return 0; + } } diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/PersonRepository.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/PersonRepository.java index 65eaca454e303..805f66f4e3db9 100644 --- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/PersonRepository.java +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/PersonRepository.java @@ -1,9 +1,14 @@ package io.quarkus.it.panache; +import java.util.List; + import javax.enterprise.context.ApplicationScoped; import io.quarkus.hibernate.orm.panache.PanacheRepository; @ApplicationScoped public class PersonRepository implements PanacheRepository { + public List findOrdered() { + return find("ORDER BY name").list(); + } } diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java index 3b51441e9a840..eca1f32696730 100644 --- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java @@ -43,6 +43,10 @@ @Path("test") public class TestEndpoint { + // fake unused injection point to force ArC to not remove this otherwise I can't mock it in the tests + @Inject + MockablePersonRepository mockablePersonRepository; + @GET @Path("model") @Transactional diff --git a/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java index 112ece19a4bee..add79fb5bd40f 100644 --- a/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java +++ b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java @@ -4,12 +4,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.StringWriter; -import java.util.Optional; -import javax.inject.Inject; import javax.json.bind.Jsonb; import javax.json.bind.JsonbBuilder; -import javax.persistence.LockModeType; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; @@ -20,7 +17,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; import io.quarkus.test.junit.DisabledOnNativeImage; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; @@ -167,35 +163,4 @@ public void testBug7721() { public void testBug8254() { RestAssured.when().get("/test/8254").then().body(is("OK")); } - - @Inject - PersonRepository realPersonRepository; - - @DisabledOnNativeImage - @Test - public void testPanacheRepositoryBridges() { - // normal method call - Assertions.assertNull(realPersonRepository.findById(0l)); - // bridge call - Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l)); - // normal method call - Assertions.assertNull(realPersonRepository.findById(0l, LockModeType.NONE)); - // bridge call - Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l, LockModeType.NONE)); - - // normal method call - Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l)); - // bridge call - Assertions.assertEquals(Optional.empty(), ((PanacheRepositoryBase) realPersonRepository).findByIdOptional(0l)); - // normal method call - Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l, LockModeType.NONE)); - // bridge call - Assertions.assertEquals(Optional.empty(), - ((PanacheRepositoryBase) realPersonRepository).findByIdOptional(0l, LockModeType.NONE)); - - // normal method call - Assertions.assertEquals(false, realPersonRepository.deleteById(0l)); - // bridge call - Assertions.assertEquals(false, ((PanacheRepositoryBase) realPersonRepository).deleteById(0l)); - } } diff --git a/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheMockingTest.java b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheMockingTest.java new file mode 100644 index 0000000000000..722070bdc989b --- /dev/null +++ b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheMockingTest.java @@ -0,0 +1,136 @@ +package io.quarkus.it.panache; + +import java.util.Collections; +import java.util.Optional; + +import javax.inject.Inject; +import javax.persistence.LockModeType; +import javax.ws.rs.WebApplicationException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import io.quarkus.panache.mock.PanacheMock; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectMock; + +@QuarkusTest +public class PanacheMockingTest { + + @Test + @Order(1) + public void testPanacheMocking() { + PanacheMock.mock(Person.class); + + Assertions.assertEquals(0, Person.count()); + + Mockito.when(Person.count()).thenReturn(23l); + Assertions.assertEquals(23, Person.count()); + + Mockito.when(Person.count()).thenReturn(42l); + Assertions.assertEquals(42, Person.count()); + + Mockito.when(Person.count()).thenCallRealMethod(); + Assertions.assertEquals(0, Person.count()); + + PanacheMock.verify(Person.class, Mockito.times(4)).count(); + + Person p = new Person(); + Mockito.when(Person.findById(12l)).thenReturn(p); + Assertions.assertSame(p, Person.findById(12l)); + Assertions.assertNull(Person.findById(42l)); + + Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException()); + try { + Person.findById(12l); + Assertions.fail(); + } catch (WebApplicationException x) { + } + + Mockito.when(Person.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(Person.findOrdered().isEmpty()); + + PanacheMock.verify(Person.class).findOrdered(); + PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any()); + PanacheMock.verifyNoMoreInteractions(Person.class); + + Assertions.assertEquals(0, Person.methodWithPrimitiveParams(true, (byte) 0, (short) 0, 0, 2, 2.0f, 2.0, 'c')); + } + + @Test + @Order(2) + public void testPanacheMockingWasCleared() { + Assertions.assertFalse(PanacheMock.IsMockEnabled); + } + + @InjectMock + MockablePersonRepository mockablePersonRepository; + + @Test + public void testPanacheRepositoryMocking() throws Throwable { + Assertions.assertEquals(0, mockablePersonRepository.count()); + + Mockito.when(mockablePersonRepository.count()).thenReturn(23l); + Assertions.assertEquals(23, mockablePersonRepository.count()); + + Mockito.when(mockablePersonRepository.count()).thenReturn(42l); + Assertions.assertEquals(42, mockablePersonRepository.count()); + + Mockito.when(mockablePersonRepository.count()).thenCallRealMethod(); + Assertions.assertEquals(0, mockablePersonRepository.count()); + + Mockito.verify(mockablePersonRepository, Mockito.times(4)).count(); + + Person p = new Person(); + Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(p); + Assertions.assertSame(p, mockablePersonRepository.findById(12l)); + Assertions.assertNull(mockablePersonRepository.findById(42l)); + + Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException()); + try { + mockablePersonRepository.findById(12l); + Assertions.fail(); + } catch (WebApplicationException x) { + } + + Mockito.when(mockablePersonRepository.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(mockablePersonRepository.findOrdered().isEmpty()); + + Mockito.verify(mockablePersonRepository).findOrdered(); + Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any()); + Mockito.verifyNoMoreInteractions(mockablePersonRepository); + } + + @Inject + PersonRepository realPersonRepository; + + @Test + public void testPanacheRepositoryBridges() { + // normal method call + Assertions.assertNull(realPersonRepository.findById(0l)); + // bridge call + Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l)); + // normal method call + Assertions.assertNull(realPersonRepository.findById(0l, LockModeType.NONE)); + // bridge call + Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l, LockModeType.NONE)); + + // normal method call + Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l)); + // bridge call + Assertions.assertEquals(Optional.empty(), ((PanacheRepositoryBase) realPersonRepository).findByIdOptional(0l)); + // normal method call + Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l, LockModeType.NONE)); + // bridge call + Assertions.assertEquals(Optional.empty(), + ((PanacheRepositoryBase) realPersonRepository).findByIdOptional(0l, LockModeType.NONE)); + + // normal method call + Assertions.assertEquals(false, realPersonRepository.deleteById(0l)); + // bridge call + Assertions.assertEquals(false, ((PanacheRepositoryBase) realPersonRepository).deleteById(0l)); + } +} diff --git a/integration-tests/mongodb-panache/pom.xml b/integration-tests/mongodb-panache/pom.xml index c5be2c1c9709a..fe0c950cfb7ab 100755 --- a/integration-tests/mongodb-panache/pom.xml +++ b/integration-tests/mongodb-panache/pom.xml @@ -44,6 +44,11 @@ quarkus-junit5 test + + io.quarkus + quarkus-panache-mock + test + io.rest-assured rest-assured diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/MockablePersonRepository.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/MockablePersonRepository.java new file mode 100644 index 0000000000000..51d645b204e97 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/MockablePersonRepository.java @@ -0,0 +1,15 @@ +package io.quarkus.it.mongodb.panache.person; + +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; +import io.quarkus.panache.common.Sort; + +@ApplicationScoped +public class MockablePersonRepository implements PanacheMongoRepositoryBase { + public List findOrdered() { + return findAll(Sort.by("lastname", "firstname")).list(); + } +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java index 4a146d9d9c1a5..be33769b45e73 100644 --- a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java @@ -1,12 +1,19 @@ package io.quarkus.it.mongodb.panache.person; +import java.util.List; + import org.bson.codecs.pojo.annotations.BsonId; import io.quarkus.mongodb.panache.PanacheMongoEntityBase; +import io.quarkus.panache.common.Sort; public class PersonEntity extends PanacheMongoEntityBase { @BsonId public Long id; public String firstname; public String lastname; + + public static List findOrdered() { + return findAll(Sort.by("lastname", "firstname")).list(); + } } diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java index e84584094bc04..8c7e1a54e3741 100644 --- a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java @@ -15,6 +15,10 @@ @Consumes(MediaType.APPLICATION_JSON) public class PersonRepositoryResource { + // fake unused injection point to force ArC to not remove this otherwise I can't mock it in the tests + @Inject + MockablePersonRepository mockablePersonRepository; + @Inject PersonRepository personRepository; diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java new file mode 100644 index 0000000000000..1f5d7ad5f72be --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheMockingTest.java @@ -0,0 +1,131 @@ +package io.quarkus.it.mongodb.panache; + +import java.util.Collections; +import java.util.Optional; + +import javax.inject.Inject; +import javax.ws.rs.WebApplicationException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.it.mongodb.panache.person.MockablePersonRepository; +import io.quarkus.it.mongodb.panache.person.PersonEntity; +import io.quarkus.it.mongodb.panache.person.PersonRepository; +import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; +import io.quarkus.panache.mock.PanacheMock; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectMock; + +@QuarkusTest +@QuarkusTestResource(MongoTestResource.class) +public class MongodbPanacheMockingTest { + + @Test + @Order(1) + public void testPanacheMocking() { + PanacheMock.mock(PersonEntity.class); + + Assertions.assertEquals(0, PersonEntity.count()); + + Mockito.when(PersonEntity.count()).thenReturn(23l); + Assertions.assertEquals(23, PersonEntity.count()); + + Mockito.when(PersonEntity.count()).thenReturn(42l); + Assertions.assertEquals(42, PersonEntity.count()); + + Mockito.when(PersonEntity.count()).thenCallRealMethod(); + Assertions.assertEquals(0, PersonEntity.count()); + + PanacheMock.verify(PersonEntity.class, Mockito.times(4)).count(); + + PersonEntity p = new PersonEntity(); + + Mockito.when(PersonEntity.findById(12l)).thenReturn(p); + Assertions.assertSame(p, PersonEntity.findById(12l)); + Assertions.assertNull(PersonEntity.findById(42l)); + + Mockito.when(PersonEntity.findById(12l)).thenThrow(new WebApplicationException()); + try { + PersonEntity.findById(12l); + Assertions.fail(); + } catch (WebApplicationException x) { + } + + Mockito.when(PersonEntity.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(PersonEntity.findOrdered().isEmpty()); + + PanacheMock.verify(PersonEntity.class).findOrdered(); + PanacheMock.verify(PersonEntity.class, Mockito.atLeastOnce()).findById(Mockito.any()); + PanacheMock.verifyNoMoreInteractions(PersonEntity.class); + } + + @Test + @Order(2) + public void testPanacheMockingWasCleared() { + Assertions.assertFalse(PanacheMock.IsMockEnabled); + } + + @InjectMock + MockablePersonRepository mockablePersonRepository; + + @Test + public void testPanacheRepositoryMocking() throws Throwable { + Assertions.assertEquals(0, mockablePersonRepository.count()); + + Mockito.when(mockablePersonRepository.count()).thenReturn(23l); + Assertions.assertEquals(23, mockablePersonRepository.count()); + + Mockito.when(mockablePersonRepository.count()).thenReturn(42l); + Assertions.assertEquals(42, mockablePersonRepository.count()); + + Mockito.when(mockablePersonRepository.count()).thenCallRealMethod(); + Assertions.assertEquals(0, mockablePersonRepository.count()); + + Mockito.verify(mockablePersonRepository, Mockito.times(4)).count(); + + PersonEntity p = new PersonEntity(); + Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(p); + Assertions.assertSame(p, mockablePersonRepository.findById(12l)); + Assertions.assertNull(mockablePersonRepository.findById(42l)); + + Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException()); + try { + mockablePersonRepository.findById(12l); + Assertions.fail(); + } catch (WebApplicationException x) { + } + + Mockito.when(mockablePersonRepository.findOrdered()).thenReturn(Collections.emptyList()); + Assertions.assertTrue(mockablePersonRepository.findOrdered().isEmpty()); + + Mockito.verify(mockablePersonRepository).findOrdered(); + Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any()); + Mockito.verifyNoMoreInteractions(mockablePersonRepository); + } + + @Inject + PersonRepository realPersonRepository; + + @Test + public void testPanacheRepositoryBridges() { + // normal method call + Assertions.assertNull(realPersonRepository.findById(0l)); + // bridge call + Assertions.assertNull(((PanacheMongoRepositoryBase) realPersonRepository).findById(0l)); + + // normal method call + Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l)); + // bridge call + Assertions.assertEquals(Optional.empty(), ((PanacheMongoRepositoryBase) realPersonRepository).findByIdOptional(0l)); + + // normal method call + Assertions.assertEquals(false, realPersonRepository.deleteById(0l)); + // bridge call + Assertions.assertEquals(false, ((PanacheMongoRepositoryBase) realPersonRepository).deleteById(0l)); + } + +} diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java index a111e25f2704a..dedf714d3983f 100644 --- a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/MongodbPanacheResourceTest.java @@ -10,9 +10,6 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.List; -import java.util.Optional; - -import javax.inject.Inject; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -24,10 +21,7 @@ import io.quarkus.it.mongodb.panache.book.BookDetail; import io.quarkus.it.mongodb.panache.person.Person; -import io.quarkus.it.mongodb.panache.person.PersonRepository; -import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; import io.quarkus.test.common.QuarkusTestResource; -import io.quarkus.test.junit.DisabledOnNativeImage; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; import io.restassured.common.mapper.TypeRef; @@ -360,26 +354,4 @@ public void testMoreEntityFunctionalities() { public void testMoreRepositoryFunctionalities() { get("/test/imperative/repository").then().statusCode(200); } - - @Inject - PersonRepository realPersonRepository; - - @DisabledOnNativeImage - @Test - public void testPanacheRepositoryBridges() { - // normal method call - Assertions.assertNull(realPersonRepository.findById(0l)); - // bridge call - Assertions.assertNull(((PanacheMongoRepositoryBase) realPersonRepository).findById(0l)); - - // normal method call - Assertions.assertEquals(Optional.empty(), realPersonRepository.findByIdOptional(0l)); - // bridge call - Assertions.assertEquals(Optional.empty(), ((PanacheMongoRepositoryBase) realPersonRepository).findByIdOptional(0l)); - - // normal method call - Assertions.assertEquals(false, realPersonRepository.deleteById(0l)); - // bridge call - Assertions.assertEquals(false, ((PanacheMongoRepositoryBase) realPersonRepository).deleteById(0l)); - } }