From 50368305dc5fa87e2d180c4459330802794b7685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=CC=8Ale=20Undheim?= Date: Thu, 6 Feb 2025 22:30:44 +0100 Subject: [PATCH] Add support in Jupiter to configure via Annotations This adds the @MatsTest annotation, as well as annotations for creating test endpoints, and to register annotated classes. This is an alternative way to configure tests in Jupiter for Mats. The tests also found that if MatsFactory or MatsFuturizer is a field, it causes conflict with Spring. Thus AbstractMatsAnnotatedClass was changed to ignore MatsFactory and MatsFuturizer, as those should not be added to the Spring context via the fields on the test instance. --- .../Extension_MatsRegistration.java | 40 ++++++ .../test/jupiter/annotation/MatsTest.java | 115 ++++++++++++++++++ .../ParameterResolver_MatsFactory.java | 29 +++++ .../ParameterResolver_MatsFuturizer.java | 29 +++++ .../PostProcessor_MatsAnnotatedClass.java | 75 ++++++++++++ .../PostProcessor_MatsEndpoint.java | 89 ++++++++++++++ ...MatsAnnotationTest_MatsAnnotatedClass.java | 115 ++++++++++++++++++ ...tationTest_MatsAnnotatedClass_Mockito.java | 88 ++++++++++++++ .../J_MatsAnnotationTest_MatsEndpoint.java | 35 ++++++ ..._MatsAnnotationTest_ParameterResolver.java | 51 ++++++++ .../AbstractMatsAnnotatedClass.java | 11 ++ 11 files changed, 677 insertions(+) create mode 100644 mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/Extension_MatsRegistration.java create mode 100644 mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/MatsTest.java create mode 100644 mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/ParameterResolver_MatsFactory.java create mode 100644 mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/ParameterResolver_MatsFuturizer.java create mode 100644 mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/PostProcessor_MatsAnnotatedClass.java create mode 100644 mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/PostProcessor_MatsEndpoint.java create mode 100644 mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_MatsAnnotatedClass.java create mode 100644 mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_MatsAnnotatedClass_Mockito.java create mode 100644 mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_MatsEndpoint.java create mode 100644 mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_ParameterResolver.java diff --git a/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/Extension_MatsRegistration.java b/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/Extension_MatsRegistration.java new file mode 100644 index 00000000..b96205b2 --- /dev/null +++ b/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/Extension_MatsRegistration.java @@ -0,0 +1,40 @@ +package io.mats3.test.jupiter.annotation; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; + +import io.mats3.serial.MatsSerializer; +import io.mats3.test.jupiter.Extension_Mats; +import io.mats3.test.jupiter.annotation.MatsTest.SerializerFactory; + +/** + * Extension to register the {@link io.mats3.test.jupiter.Extension_MatsEndpoint} via annotations. + *

+ * Since the normal {@link Extension_Mats} does not have a no-args constructor, this extension is instead + * used to register the {@link Extension_Mats} extension into a test context. + */ +class Extension_MatsRegistration implements Extension, BeforeAllCallback, AfterAllCallback { + + static final String LOG_PREFIX = "#MATSTEST:ANNOTATED# "; + + @Override + public void beforeAll(ExtensionContext context) throws ReflectiveOperationException { + MatsTest matsTest = context.getRequiredTestClass().getAnnotation(MatsTest.class); + SerializerFactory serializerFactory = matsTest.serializerFactory().getDeclaredConstructor().newInstance(); + MatsSerializer matsSerializer = serializerFactory.createSerializer(); + + Extension_Mats extensionMats = matsTest.includeDatabase() + ? Extension_Mats.createWithDb(matsSerializer) + : Extension_Mats.create(matsSerializer); + extensionMats.beforeAll(context); + } + + @Override + public void afterAll(ExtensionContext context) { + Extension_Mats extensionMats = Extension_Mats.getExtension(context); + extensionMats.afterAll(context); + } + +} diff --git a/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/MatsTest.java b/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/MatsTest.java new file mode 100644 index 00000000..6f830e9b --- /dev/null +++ b/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/MatsTest.java @@ -0,0 +1,115 @@ +package io.mats3.test.jupiter.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +import io.mats3.serial.MatsSerializer; +import io.mats3.serial.json.MatsSerializerJson; + +/** + * Annotation to provide {@link io.mats3.MatsFactory} and {@link io.mats3.util.MatsFuturizer} instances for testing. + *

+ * This annotation will allow injection of {@link io.mats3.MatsFactory} and {@link io.mats3.util.MatsFuturizer} + * instances into test constructors and test methods. This omits the need to register the + * {@link io.mats3.test.jupiter.Extension_Mats} extension. Optionally, the annotation can be used to create + * {@link io.mats3.test.jupiter.Extension_Mats} with a {@link io.mats3.serial.MatsSerializer} other than the default, + * and to also set if MATS should be set up with a database or not. + *

+ * The annotation {@link MatsTest.Endpoint} can be applied to a field of type + * {@link io.mats3.test.jupiter.Extension_MatsEndpoint} to inject a test endpoint into the test class. Thus, there + * is no need to initialize the field, as this extension will take care of resolving relevant types, and setting + * the field before the test executes. This happens after the constructor, but before any test + * and {@link org.junit.jupiter.api.BeforeEach} methods. + *

+ * The annotation {@link MatsTest.AnnotatedClass} can be applied to a field that has Mats annotations, like + * MatsMapping or MatsClassMapping. Similar to {@link io.mats3.test.jupiter.Extension_MatsAnnotatedClass}, these + * endpoints will be registered before the test executes. If the field is null, the class will be registered, and + * instantiated by the extension, as if you called + * {@link io.mats3.test.jupiter.Extension_MatsAnnotatedClass#withAnnotatedMatsClasses(Class[])}. However, if the field + * is already instantiated, the instance will be registered, as if you called {@link + * io.mats3.test.jupiter.Extension_MatsAnnotatedClass#withAnnotatedMatsInstances(Object[])}. This happens after the + * constructor and field initialization, but before any test and {@link org.junit.jupiter.api.BeforeEach} methods. + *

+ * If the test class uses mockito, and @InjectMocks is used, then this becomes sensitive to the order of the + * annotations. The MockitoExtension should be placed before the MatsTest annotation, so that it can create + * instances of the annotated classes before MatsTest inspects them. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@ExtendWith({ + Extension_MatsRegistration.class, + ParameterResolver_MatsFactory.class, + ParameterResolver_MatsFuturizer.class, + PostProcessor_MatsEndpoint.class, + PostProcessor_MatsAnnotatedClass.class +}) +public @interface MatsTest { + + /** + * Should we create the {@link io.mats3.test.jupiter.Extension_Mats} with a database or not. + * Default is no database. + * @return if the {@link io.mats3.test.jupiter.Extension_Mats} should be created with a database. + */ + boolean includeDatabase() default false; + + /** + * The serializer factory to use for the {@link io.mats3.MatsFactory} created by the extension. + * + * By default, the {@link MatsSerializerJson} is used. + * + * @return the serializer factory to use for the {@link io.mats3.MatsFactory} created by the extension. + */ + Class serializerFactory() default SerializerFactoryJson.class; + + /** + * Factory interface for creating a {@link MatsSerializer} instance. + *

+ * Note: This must have a no-args constructor. + */ + interface SerializerFactory { + + MatsSerializer createSerializer(); + } + + /** + * Default serializer factory for creating a {@link MatsSerializerJson} instance. + */ + class SerializerFactoryJson implements SerializerFactory { + @Override + public MatsSerializer createSerializer() { + return MatsSerializerJson.create(); + } + } + + /** + * Field annotation on fields of type {@link io.mats3.test.jupiter.Extension_MatsEndpoint}. + *

+ * Use this annotation to declare that a field should be injected with a test endpoint. The name of the + * endpoint should be provided as the value of the annotation. This must be an instance of + * {@link io.mats3.test.jupiter.Extension_MatsEndpoint}. If the field is already set, then the extension will + * not change the value. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface Endpoint { + + String name(); + } + + /** + * Marks a field that has Mats annotations, like MatsMapping or MatsClassMapping. + *

+ * Use this annotation to declare that a field should be registered as a Mats class. If the field is null, the + * class will be registered, and instantiated by the extension. If the field is already instantiated, the instance + * will be registered. This must be a class that has Mats annotations. + *

+ * For further documentation, see {@link io.mats3.test.jupiter.Extension_MatsAnnotatedClass}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface AnnotatedClass { } +} diff --git a/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/ParameterResolver_MatsFactory.java b/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/ParameterResolver_MatsFactory.java new file mode 100644 index 00000000..a81db36c --- /dev/null +++ b/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/ParameterResolver_MatsFactory.java @@ -0,0 +1,29 @@ +package io.mats3.test.jupiter.annotation; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; + +import io.mats3.MatsFactory; +import io.mats3.test.jupiter.Extension_Mats; + +/** + * Extension to provide a {@link MatsFactory} parameter to a test method. + *

+ * Note, this is a part of {@link MatsTest}, and should not be used directly. It requires the {@link Extension_Mats} + * to be run first. + * + * @author Ståle Undheim 2025-02-06 + */ +class ParameterResolver_MatsFactory implements ParameterResolver { + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return parameterContext.getParameter().getType() == MatsFactory.class; + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return Extension_Mats.getExtension(extensionContext).getMatsFactory(); + } +} diff --git a/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/ParameterResolver_MatsFuturizer.java b/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/ParameterResolver_MatsFuturizer.java new file mode 100644 index 00000000..e559d3bc --- /dev/null +++ b/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/ParameterResolver_MatsFuturizer.java @@ -0,0 +1,29 @@ +package io.mats3.test.jupiter.annotation; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; + +import io.mats3.test.jupiter.Extension_Mats; +import io.mats3.util.MatsFuturizer; + +/** + * Extension to provide a {@link MatsFuturizer} parameter to a test method. + *

+ * Note, this is a part of {@link MatsTest}, and should not be used directly. It requires the {@link Extension_Mats} + * to be run first. + * + * @author Ståle Undheim 2025-02-06 + */ +class ParameterResolver_MatsFuturizer implements ParameterResolver { + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return parameterContext.getParameter().getType() == MatsFuturizer.class; + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return Extension_Mats.getExtension(extensionContext).getMatsFuturizer(); + } +} diff --git a/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/PostProcessor_MatsAnnotatedClass.java b/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/PostProcessor_MatsAnnotatedClass.java new file mode 100644 index 00000000..de86fe10 --- /dev/null +++ b/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/PostProcessor_MatsAnnotatedClass.java @@ -0,0 +1,75 @@ +package io.mats3.test.jupiter.annotation; + +import static io.mats3.test.jupiter.annotation.Extension_MatsRegistration.LOG_PREFIX; + +import java.lang.reflect.Field; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.mats3.test.jupiter.Extension_Mats; +import io.mats3.test.jupiter.Extension_MatsAnnotatedClass; + +/** + * Extension to support the {@link MatsTest.AnnotatedClass} annotation on fields in a test class. + *

+ * Note, this is a part of {@link MatsTest}, and should not be used directly. It requires the {@link Extension_Mats} + * to be run first. + * + * @author Ståle Undheim 2025-02-06 + */ +class PostProcessor_MatsAnnotatedClass implements + Extension, TestInstancePostProcessor, + BeforeEachCallback, AfterEachCallback { + + private static final Logger log = LoggerFactory.getLogger(PostProcessor_MatsAnnotatedClass.class); + + private Extension_MatsAnnotatedClass _matsAnnotatedClass; + + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { + // Since postProcessTestInstance is called before beforeEach, this is where we need to instantiate + // the Extension_MatsAnnotatedClass. + _matsAnnotatedClass = Extension_MatsAnnotatedClass.create(Extension_Mats.getExtension(context)); + Field[] declaredFields = context.getRequiredTestClass().getDeclaredFields(); + for (Field declaredField : declaredFields) { + if (declaredField.isAnnotationPresent(MatsTest.AnnotatedClass.class)) { + if (!declaredField.trySetAccessible()) { + throw new IllegalStateException("Could not set accessible on field [" + declaredField + "]" + + " in test class [" + context.getRequiredTestClass() + "]" + + " We are not able to register the annotated class" + + " [" + declaredField.getType() + "]."); + } + Object fieldValue = declaredField.get(testInstance); + if (fieldValue == null) { + log.info(LOG_PREFIX + "Registering annotated field [" + declaredField.getName() + "]" + + " in test class [" + context.getRequiredTestClass() + "]" + + " without an instance as an Annotated Class."); + _matsAnnotatedClass.withAnnotatedMatsClasses(declaredField.getType()); + } + else { + log.info(LOG_PREFIX + "Registering annotated field [" + declaredField.getName() + "]" + + " in test class [" + context.getRequiredTestClass() + "]" + + " with an instance [" + fieldValue + "] as an Annotated Instance."); + _matsAnnotatedClass.withAnnotatedMatsInstances(fieldValue); + } + } + } + } + + @Override + public void beforeEach(ExtensionContext context) { + _matsAnnotatedClass.beforeEach(context); + } + + @Override + public void afterEach(ExtensionContext context) { + _matsAnnotatedClass.afterEach(context); + } + +} diff --git a/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/PostProcessor_MatsEndpoint.java b/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/PostProcessor_MatsEndpoint.java new file mode 100644 index 00000000..a895c02b --- /dev/null +++ b/mats-test-jupiter/src/main/java/io/mats3/test/jupiter/annotation/PostProcessor_MatsEndpoint.java @@ -0,0 +1,89 @@ +package io.mats3.test.jupiter.annotation; + +import static io.mats3.test.jupiter.annotation.Extension_MatsRegistration.LOG_PREFIX; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.mats3.test.jupiter.Extension_Mats; +import io.mats3.test.jupiter.Extension_MatsEndpoint; + +/** + * Extension to support the {@link MatsTest.Endpoint} annotation on fields in a test class. + *

+ * Note, this is a part of {@link MatsTest}, and should not be used directly. It requires the {@link Extension_Mats} + * to be run first. + * + * @author Ståle Undheim 2025-02-06 + */ +class PostProcessor_MatsEndpoint implements + Extension, TestInstancePostProcessor, + BeforeEachCallback, AfterEachCallback { + + private static final Logger log = LoggerFactory.getLogger(PostProcessor_MatsEndpoint.class); + + private final List> _testEndpoints = new ArrayList<>(); + + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { + Field[] declaredFields = context.getRequiredTestClass().getDeclaredFields(); + Extension_Mats extensionMats = Extension_Mats.getExtension(context); + + for (Field declaredField : declaredFields) { + if (declaredField.isAnnotationPresent(MatsTest.Endpoint.class)) { + if (!declaredField.trySetAccessible()) { + throw new IllegalStateException("Could not set accessible on field [" + declaredField + "]," + + " in test class [" + context.getRequiredTestClass() + "]." + + " We are not able to inject the MatsEndpoint into this class."); + } + if (!Extension_MatsEndpoint.class.equals(declaredField.getType())) { + throw new IllegalStateException( + "Field [" + declaredField + "] in test class [" + context.getRequiredTestClass() + "]" + + " is not of type Extension_MatsEndpoint."); + } + if (declaredField.get(testInstance) != null) { + log.debug(LOG_PREFIX + "Field [" + declaredField + "]" + + " in test class [" + context.getRequiredTestClass() + "] is already initialized"); + continue; + } + + MatsTest.Endpoint endpoint = declaredField.getAnnotation(MatsTest.Endpoint.class); + Extension_MatsEndpoint extensionMatsEndpoint = Extension_MatsEndpoint.create( + extensionMats, + endpoint.name(), + (Class) ((ParameterizedType) declaredField.getGenericType()).getActualTypeArguments()[0], + (Class) ((ParameterizedType) declaredField.getGenericType()).getActualTypeArguments()[1] + ); + log.info(LOG_PREFIX + "Injecting MatsEndpoint [" + endpoint.name() + "]" + + " into field [" + declaredField + "]" + + " in test class [" + context.getRequiredTestClass() + "]."); + _testEndpoints.add(extensionMatsEndpoint); + declaredField.set(testInstance, extensionMatsEndpoint); + } + else { + log.debug(LOG_PREFIX + "Field [" + declaredField + "] in test class [" + context.getRequiredTestClass() + + "] is not annotated with @MatsTestEndpoint, so it will not be injected."); + } + } + } + + @Override + public void beforeEach(ExtensionContext context) { + _testEndpoints.forEach(extensionMatsEndpoint -> extensionMatsEndpoint.beforeEach(context)); + } + + @Override + public void afterEach(ExtensionContext context) { + _testEndpoints.forEach(extensionMatsEndpoint -> extensionMatsEndpoint.afterEach(context)); + } +} diff --git a/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_MatsAnnotatedClass.java b/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_MatsAnnotatedClass.java new file mode 100644 index 00000000..b50521ac --- /dev/null +++ b/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_MatsAnnotatedClass.java @@ -0,0 +1,115 @@ +package io.mats3.test.jupiter.annotation; + + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.inject.Inject; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import io.mats3.spring.Dto; +import io.mats3.spring.MatsMapping; +import io.mats3.util.MatsFuturizer; +import io.mats3.util.MatsFuturizer.Reply; + +/** + * Tests the {@link MatsTest.AnnotatedClass} annotation for a field that is either instantiated by the extension, or + * already instantiated by the test class. + * + * @author Ståle Undheim 2025-02-06 + */ +@MatsTest +public class J_MatsAnnotationTest_MatsAnnotatedClass { + + private final ServiceDependency + _serviceDependency = new ServiceDependency(); + + /** + * Dummy example of a service dependency - taking a String, and prepend "Hello " to it. + */ + public static class ServiceDependency { + String formatMessage(String msg) { + return "Hello " + msg; + } + } + + public static final String ENDPOINT_ID = "AnnotatedEndpoint"; + + /** + * Example of a class with annotated MatsEndpoints. + */ + public static class AnnotatedMats3Endpoint { + private ServiceDependency _serviceDependency; + + public AnnotatedMats3Endpoint() { + /* No-args constructor for Jackson deserialization. */ + } + + @Inject + public AnnotatedMats3Endpoint(ServiceDependency serviceDependency) { + _serviceDependency = serviceDependency; + } + + @MatsMapping(ENDPOINT_ID) + public String matsEndpoint(@Dto String msg) { + return _serviceDependency.formatMessage(msg); + } + + } + + @Nested + class AnnotatedInstance { + + @MatsTest.AnnotatedClass + private final AnnotatedMats3Endpoint _annotatedMats3Endpoint = new AnnotatedMats3Endpoint(_serviceDependency); + + + @Test + void testAnnotatedMatsClass(MatsFuturizer futurizer) throws ExecutionException, InterruptedException, TimeoutException { + // :: Setup + String expectedReturn = "Hello World!"; + + // :: Act + String reply = callMatsAnnotatedEndpoint(futurizer, "World!"); + + // :: Verify + Assertions.assertEquals(expectedReturn, reply); + } + } + + @Nested + class AnnotatedClass { + + @MatsTest.AnnotatedClass + private AnnotatedMats3Endpoint _annotatedMats3Endpoint; + + @Test + void testAnnotatedMatsClass(MatsFuturizer futurizer) throws ExecutionException, InterruptedException, TimeoutException { + // :: Setup + String expectedReturn = "Hello World!"; + + // :: Act + String reply = callMatsAnnotatedEndpoint(futurizer, "World!"); + + // :: Verify + Assertions.assertEquals(expectedReturn, reply); + } + } + + + static String callMatsAnnotatedEndpoint(MatsFuturizer futurizer, String request) + throws InterruptedException, ExecutionException, TimeoutException { + return futurizer.futurizeNonessential( + "invokeAnnotatedEndpoint", + "UnitTest", + ENDPOINT_ID, + String.class, + request) + .thenApply(Reply::get) + .get(10, TimeUnit.SECONDS); + } +} diff --git a/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_MatsAnnotatedClass_Mockito.java b/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_MatsAnnotatedClass_Mockito.java new file mode 100644 index 00000000..9ac9aa7c --- /dev/null +++ b/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_MatsAnnotatedClass_Mockito.java @@ -0,0 +1,88 @@ +package io.mats3.test.jupiter.annotation; + + +import static io.mats3.test.jupiter.annotation.J_MatsAnnotationTest_MatsAnnotatedClass.callMatsAnnotatedEndpoint; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.inject.Inject; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.mats3.spring.Dto; +import io.mats3.spring.MatsMapping; +import io.mats3.util.MatsFuturizer; +import io.mats3.util.MatsFuturizer.Reply; + +/** + * Tests the {@link MatsTest.AnnotatedClass} annotation for a field that gets dependencies injected by Mockito. + * + * @author Ståle Undheim 2025-02-06 + */ +@ExtendWith(MockitoExtension.class) +@MatsTest +public class J_MatsAnnotationTest_MatsAnnotatedClass_Mockito { + + @Mock + private ServiceDependency _serviceDependency; + + @InjectMocks + @MatsTest.AnnotatedClass + private AnnotatedMats3Endpoint _annotatedMats3Endpoint; + + /** + * Dummy example of a service dependency - taking a String, and prepend "Hello " to it. + */ + public interface ServiceDependency { + String formatMessage(String msg); + } + + public static final String ENDPOINT_ID = "AnnotatedEndpoint"; + + /** + * Example of a class with annotated MatsEndpoints. + */ + public static class AnnotatedMats3Endpoint { + private ServiceDependency _serviceDependency; + + public AnnotatedMats3Endpoint() { + /* No-args constructor for Jackson deserialization. */ + } + + @Inject + public AnnotatedMats3Endpoint(ServiceDependency serviceDependency) { + _serviceDependency = serviceDependency; + } + + @MatsMapping(ENDPOINT_ID) + public String matsEndpoint(@Dto String msg) { + return _serviceDependency.formatMessage(msg); + } + + } + + @Test + void testAnnotatedMatsClass(MatsFuturizer futurizer) throws ExecutionException, InterruptedException, TimeoutException { + // :: Setup + String expectedReturn = "Hello World!"; + when(_serviceDependency.formatMessage("World!")).thenReturn(expectedReturn); + + // :: Act + String reply = callMatsAnnotatedEndpoint(futurizer, "World!"); + + // :: Verify + Assertions.assertEquals(expectedReturn, reply); + verify(_serviceDependency).formatMessage("World!"); + } + +} diff --git a/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_MatsEndpoint.java b/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_MatsEndpoint.java new file mode 100644 index 00000000..a9112693 --- /dev/null +++ b/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_MatsEndpoint.java @@ -0,0 +1,35 @@ +package io.mats3.test.jupiter.annotation; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.mats3.test.jupiter.Extension_MatsEndpoint; +import io.mats3.util.MatsFuturizer; + +/** + * Test to demonstrate how to use the {@link MatsTest.Endpoint} annotation to create a MatsEndpoint for testing. + * + * @author Ståle Undheim 2025-02-06 + */ +@MatsTest +class J_MatsAnnotationTest_MatsEndpoint { + + private static final String ENDPOINT_ID = "TestEndpoint"; + + @MatsTest.Endpoint(name = ENDPOINT_ID) + private Extension_MatsEndpoint _matsEndpoint; + + @Test + void testMatsEndpointRegistered(MatsFuturizer matsFuturizer) { + _matsEndpoint.setProcessLambda((ctx, msg) -> "Hello " + msg); + + String result = matsFuturizer.futurizeNonessential( + "testMatsEndpointRegistered", + "UnitTest", + ENDPOINT_ID, + String.class, + "World").join().get(); + + Assertions.assertEquals("Hello World", result); + } +} diff --git a/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_ParameterResolver.java b/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_ParameterResolver.java new file mode 100644 index 00000000..c7ca0a51 --- /dev/null +++ b/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/annotation/J_MatsAnnotationTest_ParameterResolver.java @@ -0,0 +1,51 @@ +package io.mats3.test.jupiter.annotation; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import io.mats3.MatsFactory; +import io.mats3.util.MatsFuturizer; + +/** + * Test to demonstrate how to use the {@link MatsTest} annotation to inject a MatsFactory or MatsFuturizer into a + * test. + * + * @author Ståle Undheim 2025-02-06 + */ +@MatsTest +class J_MatsAnnotationTest_ParameterResolver { + + @Nested + class ParameterInjection { + + @Test + void testMatsFactoryAvailable(MatsFactory matsFactory) { + Assertions.assertNotNull(matsFactory); + } + + @Test + void testMatsFuturizerAvailable(MatsFuturizer matsFuturizer) { + Assertions.assertNotNull(matsFuturizer); + } + } + + @Nested + class FieldInjection { + private final MatsFactory _matsFactory; + private final MatsFuturizer _matsFuturizer; + + FieldInjection(MatsFactory matsFactory, MatsFuturizer matsFuturizer) { + _matsFactory = matsFactory; + _matsFuturizer = matsFuturizer; + } + + @Test + void testFieldValuesSet() { + Assertions.assertNotNull(_matsFactory); + Assertions.assertNotNull(_matsFuturizer); + } + } + + +} diff --git a/mats-test/src/main/java/io/mats3/test/abstractunit/AbstractMatsAnnotatedClass.java b/mats-test/src/main/java/io/mats3/test/abstractunit/AbstractMatsAnnotatedClass.java index 6d9b0c89..661564fe 100644 --- a/mats-test/src/main/java/io/mats3/test/abstractunit/AbstractMatsAnnotatedClass.java +++ b/mats-test/src/main/java/io/mats3/test/abstractunit/AbstractMatsAnnotatedClass.java @@ -17,6 +17,7 @@ import io.mats3.MatsEndpoint; import io.mats3.MatsFactory; import io.mats3.spring.MatsSpringAnnotationRegistration; +import io.mats3.util.MatsFuturizer; /** * Base class used for Rule_MatsAnnotatedClass and Extension_MatsAnnotatedClass to support testing of classes annotated @@ -281,6 +282,16 @@ private void addTestFieldsAsBeans(Object testInstance) { addTestFieldsAsBeans(fieldInstance); return; } + if (MatsFactory.class.equals(field.getType())) { + // -> Yes, then we should not register this as a bean + if (log.isTraceEnabled()) log.trace(LOG_PREFIX + " \\- Skipping field, as it is a MatsFactory."); + return; + } + if (MatsFuturizer.class.equals(field.getType())) { + // -> Yes, then we should not register this as a bean + if (log.isTraceEnabled()) log.trace(LOG_PREFIX + " \\- Skipping field, as it is a MatsFuturizer."); + return; + } // ?: Is there no bean registered with this name, and we have a value for the field? if (!_applicationContext.containsBean(fieldName)) {