diff --git a/spring-modulith-examples/spring-modulith-example-full/src/test/java/otherpackage/integration/OrderIntegrationTests.java b/spring-modulith-integration-test/src/test/java/otherpackage/integration/TestOutsideModulePackageIntegrationTests.java similarity index 63% rename from spring-modulith-examples/spring-modulith-example-full/src/test/java/otherpackage/integration/OrderIntegrationTests.java rename to spring-modulith-integration-test/src/test/java/otherpackage/integration/TestOutsideModulePackageIntegrationTests.java index 0ba47905..630fede9 100644 --- a/spring-modulith-examples/spring-modulith-example-full/src/test/java/otherpackage/integration/OrderIntegrationTests.java +++ b/spring-modulith-integration-test/src/test/java/otherpackage/integration/TestOutsideModulePackageIntegrationTests.java @@ -15,23 +15,29 @@ */ package otherpackage.integration; -import example.order.Order; -import example.order.OrderManagement; -import lombok.RequiredArgsConstructor; +import static org.assertj.core.api.Assertions.*; + import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.modulith.test.ApplicationModuleTest; +import com.acme.myproject.moduleA.ServiceComponentA; + /** * Test to showcase {@link ApplicationModuleTest} outside of a module package + * + * @author Lukas Dohmen */ -@ApplicationModuleTest(module = "order", classes = {example.Application.class}) -@RequiredArgsConstructor -class OrderIntegrationTests { +@ApplicationModuleTest( + module = "moduleA", + classes = { com.acme.myproject.Application.class }, + verifyAutomatically = false) +class TestOutsideModulePackageIntegrationTests { - private final OrderManagement orders; + @Autowired ServiceComponentA componentA; @Test - void completesOrder() { - orders.complete(new Order()); + void injectsModuleComponent() { + assertThat(componentA).isNotNull(); } } diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ApplicationModuleTest.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ApplicationModuleTest.java index 8aa53e83..778164fe 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ApplicationModuleTest.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ApplicationModuleTest.java @@ -77,10 +77,22 @@ public @interface ApplicationModuleTest { String[] extraIncludes() default {}; /** - * Logical name of the module in case {@link ApplicationModuleTest} will be used outside a module package + * Logical name of the module to be bootstrapped in case {@link ApplicationModuleTest} will be used outside a module + * package + * + * @return will never be {@literal null}. + * @since 1.3 */ String module() default ""; + /** + * Alias for {@link SpringBootTest#classes()}. Useful to define the main application class in case the test is located + * outside a module package and that class doesn't reside in any of the parent packages. + * + * @return will never be {@literal null}. + * @see #module() + * @since 1.3 + */ @AliasFor(annotation = SpringBootTest.class) Class[] classes() default {}; diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java index 589c6f29..5327ebce 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.slf4j.Logger; @@ -34,11 +35,13 @@ import org.springframework.modulith.core.ApplicationModule; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.core.JavaPackage; import org.springframework.modulith.test.ApplicationModuleTest.BootstrapMode; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.util.function.SingletonSupplier; /** * @author Oliver Drotbohm + * @author Lukas Dohmen */ public class ModuleTestExecution implements Iterable { @@ -95,21 +98,14 @@ public class ModuleTestExecution implements Iterable { var annotation = AnnotatedElementUtils.findMergedAnnotation(type, ApplicationModuleTest.class); var packageName = type.getPackage().getName(); - var optionalModulithType = findSpringBootApplicationByClasses(annotation); - - var modulithType = optionalModulithType.orElseGet(() -> MODULITH_TYPES.computeIfAbsent(type, - it -> new AnnotatedClassFinder(SpringBootApplication.class).findFromPackage(packageName))); - var modules = ApplicationModules.of(modulithType); - + var modules = ApplicationModules.of(findSpringBootApplicationByClasses(annotation, type)); var moduleName = annotation.module(); - ApplicationModule module; - if (StringUtils.hasText(moduleName)) { - module = modules.getModuleByName(moduleName).orElseThrow( // - () -> new IllegalStateException(String.format("Unable to find module %s!", moduleName))); - } else { - module = modules.getModuleForPackage(packageName).orElseThrow( // - () -> new IllegalStateException(String.format("Package %s is not part of any module!", packageName))); - } + + var module = StringUtils.hasText(moduleName) + ? modules.getModuleByName(moduleName).orElseThrow( // + () -> new IllegalStateException(String.format("Unable to find module %s!", moduleName))) + : modules.getModuleForPackage(packageName).orElseThrow( // + () -> new IllegalStateException(String.format("Package %s is not part of any module!", packageName))); return EXECUTIONS.computeIfAbsent(new Key(module.getBasePackage().getName(), annotation), it -> new ModuleTestExecution(annotation, modules, module)); @@ -240,15 +236,22 @@ public class ModuleTestExecution implements Iterable { .flatMap(Optional::stream); } - private static Optional> findSpringBootApplicationByClasses(ApplicationModuleTest annotation) { - for (Class clazz : annotation.classes()) { - Class modulithType = MODULITH_TYPES.computeIfAbsent(clazz, - it -> new AnnotatedClassFinder(SpringBootApplication.class).findFromPackage(clazz.getPackageName())); - if (modulithType != null) { - return Optional.of(modulithType); - } - } - return Optional.empty(); + private static Class findSpringBootApplicationByClasses(ApplicationModuleTest annotation, + Class testClass) { + + var types = ObjectUtils.addObjectToArray(annotation.classes(), testClass); + + return Arrays.stream(types) + .> map(ModuleTestExecution::lookupSpringBootApplicationAnnotation) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Couldn't find @SpringBootApplication traversing %s." + .formatted(Arrays.stream(types).map(Class::getName).collect(Collectors.joining(", "))))); + } + + private static Class lookupSpringBootApplicationAnnotation(Class clazz) { + + return MODULITH_TYPES.computeIfAbsent(clazz, + it -> new AnnotatedClassFinder(SpringBootApplication.class).findFromClass(clazz)); } private static record Key(String moduleBasePackage, ApplicationModuleTest annotation) {}