From 010cd684e3bdd9f718568ce52892bf63c16c06b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=CC=8Ale=20Undheim?= Date: Thu, 9 Jan 2025 15:01:43 +0100 Subject: [PATCH] Added tests for MatsAnnoatedClass test helper Added example tests for both jUnit and Jupiter showing various ways that the Extension/Rule can be used. Also fixed so that AbstractMatsAnnotatedClass will get fields from the encapsualting class to include in the Spring context, for nested tests in Jupiter. --- mats-test-junit/build.gradle | 5 + .../junit/U_RuleMatsAnnotatedClassTest.java | 107 ++++++++++++ mats-test-jupiter/build.gradle | 4 + .../J_ExtensionMatsAnnotatedClassTest.java | 161 ++++++++++++++++++ .../AbstractMatsAnnotatedClass.java | 40 +++-- 5 files changed, 306 insertions(+), 11 deletions(-) create mode 100644 mats-test-junit/src/test/java/io/mats3/test/junit/U_RuleMatsAnnotatedClassTest.java create mode 100644 mats-test-jupiter/src/test/java/io/mats3/test/jupiter/J_ExtensionMatsAnnotatedClassTest.java diff --git a/mats-test-junit/build.gradle b/mats-test-junit/build.gradle index c8b1e884..d538cf49 100644 --- a/mats-test-junit/build.gradle +++ b/mats-test-junit/build.gradle @@ -27,6 +27,9 @@ dependencies { testImplementation project(':mats-util') testImplementation project(':mats-test-broker') + // To test MatsAnnotatedClass + testImplementation project(':mats-spring') + // ..Removed in Java 11 testImplementation "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" @@ -41,6 +44,8 @@ dependencies { // Single Spring test inside testImplementation "org.springframework:spring-test:$springVersion" testImplementation "org.springframework:spring-context:$springVersion" + testImplementation "org.springframework:spring-tx:$springVersion" + } publishing { diff --git a/mats-test-junit/src/test/java/io/mats3/test/junit/U_RuleMatsAnnotatedClassTest.java b/mats-test-junit/src/test/java/io/mats3/test/junit/U_RuleMatsAnnotatedClassTest.java new file mode 100644 index 00000000..832f5027 --- /dev/null +++ b/mats-test-junit/src/test/java/io/mats3/test/junit/U_RuleMatsAnnotatedClassTest.java @@ -0,0 +1,107 @@ +package io.mats3.test.junit; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.inject.Inject; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; + +import io.mats3.spring.Dto; +import io.mats3.spring.MatsMapping; +import io.mats3.util.MatsFuturizer.Reply; + +/** + * Test of {@link Rule_MatsAnnotatedClass} + * + * @author Ståle Undheim 2025-01-09 + */ +public class U_RuleMatsAnnotatedClassTest { + + @ClassRule + public static final Rule_Mats MATS = Rule_Mats.create(); + public static final String ENDPOINT_ID = "AnnotatedEndpoint"; + + public static class ServiceDependency { + String formatMessage(String msg) { + return "Hello " + msg; + } + } + + public static class MatsAnnotatedClass_Endpoint { + + private ServiceDependency _serviceDependency; + + public MatsAnnotatedClass_Endpoint() { + + } + + @Inject + public MatsAnnotatedClass_Endpoint(ServiceDependency serviceDependency) { + _serviceDependency = serviceDependency; + } + + @MatsMapping(ENDPOINT_ID) + public String matsEndpoint(@Dto String msg) { + return _serviceDependency.formatMessage(msg); + } + + } + + // This dependency will be picked up by Extension_MatsAnnotatedClass, and result in injecting this + // instance into the service. This would also work if this was instead a Mockito mock. + private final ServiceDependency _serviceDependency = new ServiceDependency(); + + @Rule + public final Rule_MatsAnnotatedClass _matsAnnotationRule = Rule_MatsAnnotatedClass.create(MATS); + + /** + * Example of adding a mats annotated class inside a test case, that will then be created and started. + */ + @Test + public void testAnnotatedMatsClass() throws ExecutionException, InterruptedException, TimeoutException { + // :: Setup + _matsAnnotationRule.withAnnotatedMatsClasses(MatsAnnotatedClass_Endpoint.class); + String expectedReturn = "Hello World!"; + + // :: Act + String reply = callMatsAnnotatedEndpoint("World!"); + + // :: Verify + Assert.assertEquals(expectedReturn, reply); + } + + /** + * Example of using an already instantiated Mats annotated class inside a test method. + */ + @Test + public void testAnnotatedMatsInstance() throws ExecutionException, InterruptedException, TimeoutException { + // :: Setup + MatsAnnotatedClass_Endpoint annotatedMatsInstance = new MatsAnnotatedClass_Endpoint(_serviceDependency); + _matsAnnotationRule.withAnnotatedMatsInstances(annotatedMatsInstance); + String expectedReturn = "Hello World!"; + + // :: Act + String reply = callMatsAnnotatedEndpoint("World!"); + + // :: Verify + Assert.assertEquals(expectedReturn, reply); + } + + private static String callMatsAnnotatedEndpoint(String message) + throws InterruptedException, ExecutionException, TimeoutException { + return MATS.getMatsFuturizer().futurizeNonessential( + "invokeAnnotatedEndpoint", + U_RuleMatsAnnotatedClassTest.class.getSimpleName(), + ENDPOINT_ID, + String.class, + message) + .thenApply(Reply::getReply) + .get(10, TimeUnit.SECONDS); + } + +} diff --git a/mats-test-jupiter/build.gradle b/mats-test-jupiter/build.gradle index 92127b27..f51672c8 100644 --- a/mats-test-jupiter/build.gradle +++ b/mats-test-jupiter/build.gradle @@ -27,6 +27,9 @@ dependencies { testImplementation project(':mats-util') testImplementation project(':mats-test-broker') + // To test MatsAnnotatedClass + testImplementation project(':mats-spring') + // ..Removed in Java 11 testImplementation "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" @@ -43,6 +46,7 @@ dependencies { exclude group:'junit', module:'junit' } testImplementation "org.springframework:spring-context:$springVersion" + testImplementation "org.springframework:spring-tx:$springVersion" // The Jupiter Runtime..? testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" diff --git a/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/J_ExtensionMatsAnnotatedClassTest.java b/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/J_ExtensionMatsAnnotatedClassTest.java new file mode 100644 index 00000000..178d204f --- /dev/null +++ b/mats-test-jupiter/src/test/java/io/mats3/test/jupiter/J_ExtensionMatsAnnotatedClassTest.java @@ -0,0 +1,161 @@ +package io.mats3.test.jupiter; + +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.RegisterExtension; + +import io.mats3.spring.Dto; +import io.mats3.spring.MatsMapping; +import io.mats3.util.MatsFuturizer.Reply; + +/** + * Test of {@link Extension_MatsAnnotatedClass}, with example of adding annotated classes when the extension is + * created, or within each test method. + * + * @author Ståle Undheim 2025-01-09 + */ +class J_ExtensionMatsAnnotatedClassTest { + + @RegisterExtension + private static final Extension_Mats MATS = Extension_Mats.create(); + public static final String ENDPOINT_ID = "AnnotatedEndpoint"; + + public static class ServiceDependency { + String formatMessage(String msg) { + return "Hello " + msg; + } + } + + public static class MatsAnnotatedClass_Endpoint { + + private ServiceDependency _serviceDependency; + + public MatsAnnotatedClass_Endpoint() { + + } + + @Inject + public MatsAnnotatedClass_Endpoint(ServiceDependency serviceDependency) { + _serviceDependency = serviceDependency; + } + + @MatsMapping(ENDPOINT_ID) + public String matsEndpoint(@Dto String msg) { + return _serviceDependency.formatMessage(msg); + } + + } + + // This dependency will be picked up by Extension_MatsAnnotatedClass, and result in injecting this + // instance into the service. This would also work if this was instead a Mockito mock. + private final ServiceDependency _serviceDependency = new ServiceDependency(); + + + /** + * Example for how to provide a class with annotated MatsEndpoints to test. + */ + @Nested + class TestAnnotatedWithProvidedClass { + @RegisterExtension + private final Extension_MatsAnnotatedClass _matsAnnotatedClassExtension = Extension_MatsAnnotatedClass + .create(MATS) + .withAnnotatedMatsClasses(MatsAnnotatedClass_Endpoint.class); + + @Test + void testAnnotatedMatsClass() throws ExecutionException, InterruptedException, TimeoutException { + // :: Setup + String expectedReturn = "Hello World!"; + + // :: Act + String reply = callMatsAnnotatedEndpoint("World!"); + + // :: Verify + Assertions.assertEquals(expectedReturn, reply); + } + } + + /** + * Example for how to provide an instance of an annotated class with MatsEndpoints to test. + */ + @Nested + class TestAnnotatedWithProvidedInstance { + + @RegisterExtension + private final Extension_MatsAnnotatedClass _matsAnnotatedClassExtension = Extension_MatsAnnotatedClass + .create(MATS) + .withAnnotatedMatsInstances(new MatsAnnotatedClass_Endpoint(_serviceDependency)); + + @Test + void testAnnotatedMatsInstance() throws ExecutionException, InterruptedException, TimeoutException { + // :: Setup + String expectedReturn = "Hello World!"; + + // :: Act + String reply = callMatsAnnotatedEndpoint("World!"); + + // :: Verify + Assertions.assertEquals(expectedReturn, reply); + } + } + + /** + * These tests demonstrate that we can add new annotated classes within a test. + */ + @Nested + class TestAnnotatedWithDelayedConfiguration { + + @RegisterExtension + private final Extension_MatsAnnotatedClass _matsAnnotatedClassExtension + = Extension_MatsAnnotatedClass.create(MATS); + + @Test + void testAnnotatedMatsClass() throws ExecutionException, InterruptedException, TimeoutException { + // :: Setup + _matsAnnotatedClassExtension.withAnnotatedMatsClasses(MatsAnnotatedClass_Endpoint.class); + String expectedReturn = "Hello World!"; + + // :: Act + String reply = callMatsAnnotatedEndpoint("World!"); + + // :: Verify + Assertions.assertEquals(expectedReturn, reply); + } + + + @Test + void testAnnotatedMatsInstance() throws ExecutionException, InterruptedException, TimeoutException { + // :: Setup + MatsAnnotatedClass_Endpoint annotatedClassInstance = + new MatsAnnotatedClass_Endpoint(_serviceDependency); + _matsAnnotatedClassExtension.withAnnotatedMatsInstances(annotatedClassInstance); + String expectedReturn = "Hello World!"; + + // :: Act + String reply = callMatsAnnotatedEndpoint("World!"); + + // :: Verify + Assertions.assertEquals(expectedReturn, reply); + } + } + + private String callMatsAnnotatedEndpoint(String message) + throws InterruptedException, ExecutionException, TimeoutException { + return MATS.getMatsFuturizer().futurizeNonessential( + "invokeAnnotatedEndpoint", + getClass().getSimpleName(), + ENDPOINT_ID, + String.class, + message) + .thenApply(Reply::getReply) + .get(10, TimeUnit.SECONDS); + } + + +} 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 a55d453b..380dcf44 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 @@ -47,21 +47,39 @@ public void beforeEach(Object testInstance) { // If we have a test instance, we register each non-null field as a singleton in the Spring context. // This is so that those fields are available to inject into the Mats annotated classes. if (testInstance != null) { - ReflectionUtils.doWithFields(testInstance.getClass(), field -> { - field.setAccessible(true); - String beanName = field.getName(); - Object beanInstance = field.get(testInstance); - - // ?: Is there no bean registered with this name, and we have a value for the field? - if (!_applicationContext.containsBean(beanName) && beanInstance != null) { - // Yes -> add this to the Spring context as a singleton - _applicationContext.getBeanFactory().registerSingleton(beanName, beanInstance); - } - }); + addBeans(testInstance); } initializeBeansAndRegisterEndpoints(); } + private void addBeans(Object testInstance) { + Class enclosingClass = testInstance.getClass().getEnclosingClass(); + + ReflectionUtils.doWithFields(testInstance.getClass(), field -> { + field.setAccessible(true); + String beanName = field.getName(); + Object beanInstance = field.get(testInstance); + // Do not inject this utility into Spring + if (beanInstance == this) { + return; + } + + // ?: Is this a synthetic field created by the compiler for the enclosing class? + if (field.getType().equals(enclosingClass) && field.isSynthetic()) { + // Yes, then add the fields from the enclosing class as well to the Spring context (but not + // the test itself). This is to support nested tests in Jupiter. + addBeans(beanInstance); + return; + } + + // ?: Is there no bean registered with this name, and we have a value for the field? + if (!_applicationContext.containsBean(beanName) && beanInstance != null) { + // Yes -> add this to the Spring context as a singleton + _applicationContext.getBeanFactory().registerSingleton(beanName, beanInstance); + } + }); + } + public void afterEach() { for (MatsEndpoint endpoint : _registeredEndpoints) { endpoint.remove(30_000);