GH-1 - Initial port of Moduliths project.
Basically the state of commit c7cf939 of https://github.com/moduliths/moduliths for further development under the Spring umbrella.
This commit is contained in:
60
moduliths-test/pom.xml
Normal file
60
moduliths-test/pom.xml
Normal file
@@ -0,0 +1,60 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.moduliths</groupId>
|
||||
<artifactId>moduliths</artifactId>
|
||||
<version>1.4.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>Moduliths - Test</name>
|
||||
<artifactId>moduliths-test</artifactId>
|
||||
|
||||
<properties>
|
||||
<module.name>org.moduliths.test</module.name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>moduliths-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-test-autoconfigure</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-commons</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.data.domain.DomainEvents;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.ReflectionUtils.MethodCallback;
|
||||
|
||||
/**
|
||||
* Test utilities to work with aggregates.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public class AggregateTestUtils {
|
||||
|
||||
private static Map<Class<?>, Optional<Method>> CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Extracts all domain events from the given aggregate that uses Spring Data's {@link DomainEvents} annotation to
|
||||
* expose them.
|
||||
*
|
||||
* @param aggregate must not be {@literal null}.
|
||||
* @return {@link PublishedEvents} for all events contained in the given aggregate, will never be {@literal null}.
|
||||
*/
|
||||
public static PublishedEvents eventsOf(Object aggregate) {
|
||||
|
||||
Collection<?> events = CACHE.computeIfAbsent(aggregate.getClass(), AggregateTestUtils::findAnnotatedMethod)
|
||||
.map(it -> ReflectionUtils.invokeMethod(it, aggregate)) //
|
||||
.map(Collection.class::cast) //
|
||||
.orElseGet(Collections::emptyList);
|
||||
|
||||
return PublishedEvents.of(events);
|
||||
}
|
||||
|
||||
private static Optional<Method> findAnnotatedMethod(Class<?> type) {
|
||||
|
||||
DomainEventsMethodFinder finder = new DomainEventsMethodFinder();
|
||||
ReflectionUtils.doWithMethods(type, finder);
|
||||
|
||||
return Optional.ofNullable(finder.method);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link MethodCallback} to find a method annotated with {@link DomainEvents}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
private static class DomainEventsMethodFinder implements MethodCallback {
|
||||
|
||||
Method method;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.util.ReflectionUtils.MethodCallback#doWith(java.lang.reflect.Method)
|
||||
*/
|
||||
@Override
|
||||
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
|
||||
|
||||
if (this.method != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (method.isAnnotationPresent(DomainEvents.class)) {
|
||||
this.method = method;
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2019-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.test;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.PayloadApplicationEvent;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link PublishedEvents}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class DefaultPublishedEvents implements PublishedEvents, ApplicationListener<ApplicationEvent> {
|
||||
|
||||
private final List<Object> events;
|
||||
|
||||
/**
|
||||
* Creates a new, empty {@link DefaultPublishedEvents} instance.
|
||||
*/
|
||||
DefaultPublishedEvents() {
|
||||
this(Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link DefaultPublishedEvents} instance with the given events.
|
||||
*
|
||||
* @param events must not be {@literal null}.
|
||||
*/
|
||||
DefaultPublishedEvents(Collection<? extends Object> events) {
|
||||
|
||||
Assert.notNull(events, "Events must not be null!");
|
||||
|
||||
this.events = new ArrayList<>(events);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
|
||||
*/
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
this.events.add(unwrapPayloadEvent(event));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.test.PublishedEvents#ofType(java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> TypedPublishedEvents<T> ofType(Class<T> type) {
|
||||
|
||||
return SimpleTypedPublishedEvents.of(events.stream()//
|
||||
.filter(type::isInstance) //
|
||||
.map(type::cast));
|
||||
}
|
||||
|
||||
private static Object unwrapPayloadEvent(Object source) {
|
||||
|
||||
return PayloadApplicationEvent.class.isInstance(source) //
|
||||
? ((PayloadApplicationEvent<?>) source).getPayload() //
|
||||
: source;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor(staticName = "of")
|
||||
private static class SimpleTypedPublishedEvents<T> implements TypedPublishedEvents<T> {
|
||||
|
||||
private final List<T> events;
|
||||
|
||||
private static <T> SimpleTypedPublishedEvents<T> of(Stream<T> stream) {
|
||||
return new SimpleTypedPublishedEvents<>(stream.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.test.PublishedEvents.TypedPublishedEvents#ofSubType(java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <S extends T> TypedPublishedEvents<S> ofSubType(Class<S> subType) {
|
||||
|
||||
return SimpleTypedPublishedEvents.of(getFilteredEvents(subType::isInstance) //
|
||||
.map(subType::cast));
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.test.PublishedEvents.TypedPublishedEvents#matching(java.util.function.Predicate)
|
||||
*/
|
||||
@Override
|
||||
public TypedPublishedEvents<T> matching(Predicate<? super T> predicate) {
|
||||
return SimpleTypedPublishedEvents.of(getFilteredEvents(predicate));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.test.PublishedEvents.TypedPublishedEvents#matchingMapped(java.util.function.Function, java.util.function.Predicate)
|
||||
*/
|
||||
@Override
|
||||
public <S> TypedPublishedEvents<T> matchingMapped(Function<T, S> mapper, Predicate<? super S> predicate) {
|
||||
|
||||
return SimpleTypedPublishedEvents.of(events.stream().flatMap(it -> {
|
||||
|
||||
S mapped = mapper.apply(it);
|
||||
|
||||
return predicate.test(mapped) ? Stream.of(it) : Stream.empty();
|
||||
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Stream} of events filtered by the given {@link Predicate}.
|
||||
*
|
||||
* @param predicate must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private Stream<T> getFilteredEvents(Predicate<? super T> predicate) {
|
||||
return events.stream().filter(predicate);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
*/
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return events.iterator();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return events.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.test;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.model.Module;
|
||||
import org.moduliths.model.Modules;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.test.context.ContextConfigurationAttributes;
|
||||
import org.springframework.test.context.ContextCustomizer;
|
||||
import org.springframework.test.context.ContextCustomizerFactory;
|
||||
import org.springframework.test.context.MergedContextConfiguration;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
class ModuleContextCustomizerFactory implements ContextCustomizerFactory {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.test.context.ContextCustomizerFactory#createContextCustomizer(java.lang.Class, java.util.List)
|
||||
*/
|
||||
@Override
|
||||
public ContextCustomizer createContextCustomizer(Class<?> testClass,
|
||||
List<ContextConfigurationAttributes> configAttributes) {
|
||||
|
||||
ModuleTest moduleTest = AnnotatedElementUtils.getMergedAnnotation(testClass, ModuleTest.class);
|
||||
|
||||
return moduleTest == null ? null : new ModuleContextCustomizer(testClass);
|
||||
}
|
||||
|
||||
@Slf4j
|
||||
@EqualsAndHashCode
|
||||
static class ModuleContextCustomizer implements ContextCustomizer {
|
||||
|
||||
private static final String BEAN_NAME = ModuleTestExecution.class.getName();
|
||||
|
||||
private final Supplier<ModuleTestExecution> execution;
|
||||
|
||||
private ModuleContextCustomizer(Class<?> testClass) {
|
||||
this.execution = ModuleTestExecution.of(testClass);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.test.context.ContextCustomizer#customizeContext(org.springframework.context.ConfigurableApplicationContext, org.springframework.test.context.MergedContextConfiguration)
|
||||
*/
|
||||
@Override
|
||||
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
|
||||
|
||||
ModuleTestExecution testExecution = execution.get();
|
||||
|
||||
logModules(testExecution);
|
||||
|
||||
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
|
||||
beanFactory.registerSingleton(BEAN_NAME, testExecution);
|
||||
|
||||
DefaultPublishedEvents events = new DefaultPublishedEvents();
|
||||
beanFactory.registerSingleton(events.getClass().getName(), events);
|
||||
context.addApplicationListener(events);
|
||||
}
|
||||
|
||||
private static void logModules(ModuleTestExecution execution) {
|
||||
|
||||
Module module = execution.getModule();
|
||||
Modules modules = execution.getModules();
|
||||
String moduleName = module.getDisplayName();
|
||||
String bootstrapMode = execution.getBootstrapMode().name();
|
||||
|
||||
String message = String.format("Bootstrapping @ModuleTest for %s in mode %s (%s)…", moduleName, bootstrapMode,
|
||||
modules.getModulithSource());
|
||||
|
||||
LOG.info(message);
|
||||
LOG.info(getSeparator("=", message));
|
||||
|
||||
Arrays.stream(module.toString(modules).split("\n")).forEach(LOG::info);
|
||||
|
||||
List<Module> extraIncludes = execution.getExtraIncludes();
|
||||
|
||||
if (!extraIncludes.isEmpty()) {
|
||||
|
||||
logHeadline("Extra includes:", message);
|
||||
|
||||
extraIncludes.forEach(it -> LOG.info("> ".concat(it.getName())));
|
||||
}
|
||||
|
||||
Set<Module> sharedModules = modules.getSharedModules();
|
||||
|
||||
if (!sharedModules.isEmpty()) {
|
||||
|
||||
logHeadline("Shared modules:", message);
|
||||
|
||||
sharedModules.forEach(it -> LOG.info("> ".concat(it.getName())));
|
||||
}
|
||||
|
||||
List<Module> dependencies = execution.getDependencies();
|
||||
|
||||
if (!dependencies.isEmpty() || !sharedModules.isEmpty()) {
|
||||
|
||||
logHeadline("Included dependencies:", message);
|
||||
|
||||
Stream<Module> dependenciesPlusMissingSharedOnes = //
|
||||
Stream.concat(dependencies.stream(), sharedModules.stream() //
|
||||
.filter(it -> !dependencies.contains(it)));
|
||||
|
||||
dependenciesPlusMissingSharedOnes //
|
||||
.map(it -> it.toString(modules)) //
|
||||
.forEach(it -> {
|
||||
Arrays.stream(it.split("\n")).forEach(LOG::info);
|
||||
});
|
||||
|
||||
LOG.info(getSeparator("=", message));
|
||||
}
|
||||
}
|
||||
|
||||
private static String getSeparator(String character, String reference) {
|
||||
return String.join("", Collections.nCopies(reference.length(), character));
|
||||
}
|
||||
|
||||
private static void logHeadline(String headline, String reference) {
|
||||
logHeadline(headline, reference, () -> {});
|
||||
}
|
||||
|
||||
private static void logHeadline(String headline, String reference, Runnable additional) {
|
||||
|
||||
LOG.info(getSeparator("=", reference));
|
||||
LOG.info(headline);
|
||||
additional.run();
|
||||
LOG.info(getSeparator("=", reference));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.test;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.junit.jupiter.api.TestInstance.Lifecycle;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.moduliths.model.Module.DependencyDepth;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters;
|
||||
import org.springframework.boot.test.context.SpringBootTestContextBootstrapper;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.test.context.BootstrapWith;
|
||||
import org.springframework.test.context.TestConstructor;
|
||||
import org.springframework.test.context.TestConstructor.AutowireMode;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
/**
|
||||
* Bootstraps the module containing the package of the test class annotated with {@link ModuleTest}. Will apply the
|
||||
* following modifications to the Spring Boot configuration:
|
||||
* <ul>
|
||||
* <li>Restricts the component scanning to the module's package.
|
||||
* <li>
|
||||
* <li>Sets the module's package as the only auto-configuration and entity scan package.
|
||||
* <li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@BootstrapWith(SpringBootTestContextBootstrapper.class)
|
||||
@TypeExcludeFilters(ModuleTypeExcludeFilter.class)
|
||||
@ImportAutoConfiguration(ModuleTestAutoConfiguration.class)
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ExtendWith(PublishedEventsParameterResolver.class)
|
||||
@TestInstance(Lifecycle.PER_CLASS)
|
||||
@TestConstructor(autowireMode = AutowireMode.ALL)
|
||||
public @interface ModuleTest {
|
||||
|
||||
@AliasFor("mode")
|
||||
BootstrapMode value() default BootstrapMode.STANDALONE;
|
||||
|
||||
@AliasFor("value")
|
||||
BootstrapMode mode() default BootstrapMode.STANDALONE;
|
||||
|
||||
/**
|
||||
* Whether to automatically verify the module structure for validity.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
boolean verifyAutomatically() default true;
|
||||
|
||||
/**
|
||||
* Module names of modules to be included in the test run independent of what the {@link #mode()} defines.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String[] extraIncludes() default {};
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public enum BootstrapMode {
|
||||
|
||||
/**
|
||||
* Boorstraps the current module only.
|
||||
*/
|
||||
STANDALONE(DependencyDepth.NONE),
|
||||
|
||||
/**
|
||||
* Bootstraps the current module as well as its direct dependencies.
|
||||
*/
|
||||
DIRECT_DEPENDENCIES(DependencyDepth.IMMEDIATE),
|
||||
|
||||
/**
|
||||
* Bootstraps the current module as well as all upstream dependencies (including transitive ones).
|
||||
*/
|
||||
ALL_DEPENDENCIES(DependencyDepth.ALL);
|
||||
|
||||
private final @Getter DependencyDepth depth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.test;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An unconditional auto-configuration registering an {@link ImportBeanDefinitionRegistrar} to customize both the entity
|
||||
* scan and auto-configuration packages to the packages defined by the {@link ModuleTestExecution} in the application
|
||||
* context.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Configuration
|
||||
@Import(ModuleTestAutoConfiguration.AutoConfigurationAndEntityScanPackageCustomizer.class)
|
||||
class ModuleTestAutoConfiguration {
|
||||
|
||||
private static final String AUTOCONFIG_PACKAGES = "org.springframework.boot.autoconfigure.AutoConfigurationPackages";
|
||||
private static final String ENTITY_SCAN_PACKAGE = "org.springframework.boot.autoconfigure.domain.EntityScanPackages";
|
||||
|
||||
@Slf4j
|
||||
static class AutoConfigurationAndEntityScanPackageCustomizer implements ImportBeanDefinitionRegistrar {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry)
|
||||
*/
|
||||
@Override
|
||||
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
|
||||
|
||||
ModuleTestExecution execution = ((BeanFactory) registry).getBean(ModuleTestExecution.class);
|
||||
List<String> basePackages = execution.getBasePackages().collect(Collectors.toList());
|
||||
|
||||
LOG.info("Re-configuring auto-configuration and entity scan packages to: {}.",
|
||||
StringUtils.collectionToDelimitedString(basePackages, ", "));
|
||||
|
||||
setBasePackagesOn(registry, AUTOCONFIG_PACKAGES, "BasePackagesBeanDefinition", "basePackages", basePackages);
|
||||
setBasePackagesOn(registry, ENTITY_SCAN_PACKAGE, "EntityScanPackagesBeanDefinition", "packageNames",
|
||||
basePackages);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void setBasePackagesOn(BeanDefinitionRegistry registry, String beanName, String definitionType,
|
||||
String fieldName, List<String> packages) {
|
||||
|
||||
if (!registry.containsBeanDefinition(beanName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
BeanDefinition definition = registry.getBeanDefinition(beanName);
|
||||
|
||||
// For Boot 2.4, we deal with a BasePackagesBeanDefinition
|
||||
Field field = Arrays.stream(definition.getClass().getDeclaredFields())
|
||||
.filter(__ -> definition.getClass().getSimpleName().equals(definitionType))
|
||||
.filter(it -> it.getName().equals(fieldName))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (field != null) {
|
||||
|
||||
// Keep all auto-configuration packages from Moduliths
|
||||
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
((Set<String>) ReflectionUtils.getField(field, definition)).stream()
|
||||
.filter(it -> it.startsWith("org.moduliths"))
|
||||
.forEach(packages::add);
|
||||
|
||||
ReflectionUtils.setField(field, definition, new HashSet<>(packages));
|
||||
|
||||
} else {
|
||||
|
||||
ValueHolder holder = definition.getConstructorArgumentValues().getArgumentValue(0, String[].class);
|
||||
Arrays.stream((String[]) holder.getValue())
|
||||
.filter(it -> it.startsWith("org.moduliths"))
|
||||
.forEach(packages::add);
|
||||
|
||||
// Fall back to customize the bean definition in a Boot 2.3 arrangement
|
||||
definition.getConstructorArgumentValues().addIndexedArgumentValue(0, packages);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2018-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.test;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.model.JavaPackage;
|
||||
import org.moduliths.model.Module;
|
||||
import org.moduliths.model.Modules;
|
||||
import org.moduliths.test.ModuleTest.BootstrapMode;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.test.context.AnnotatedClassFinder;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
|
||||
import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier;
|
||||
import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Slf4j
|
||||
@EqualsAndHashCode(of = "key")
|
||||
public class ModuleTestExecution implements Iterable<Module> {
|
||||
|
||||
private static Map<Class<?>, Class<?>> MODULITH_TYPES = new HashMap<>();
|
||||
private static Map<Key, ModuleTestExecution> EXECUTIONS = new HashMap<>();
|
||||
|
||||
private final Key key;
|
||||
|
||||
private final @Getter BootstrapMode bootstrapMode;
|
||||
private final @Getter Module module;
|
||||
private final @Getter Modules modules;
|
||||
private final @Getter List<Module> extraIncludes;
|
||||
|
||||
private final Supplier<List<JavaPackage>> basePackages;
|
||||
private final Supplier<List<Module>> dependencies;
|
||||
|
||||
private ModuleTestExecution(ModuleTest annotation, Modules modules, Module module) {
|
||||
|
||||
this.key = Key.of(module.getBasePackage().getName(), annotation);
|
||||
this.modules = modules;
|
||||
this.bootstrapMode = annotation.mode();
|
||||
this.module = module;
|
||||
|
||||
this.extraIncludes = getExtraModules(annotation, modules).collect(Collectors.toList());
|
||||
|
||||
this.basePackages = Suppliers.memoize(() -> {
|
||||
|
||||
Stream<JavaPackage> moduleBasePackages = module.getBasePackages(modules, bootstrapMode.getDepth());
|
||||
Stream<JavaPackage> sharedBasePackages = modules.getSharedModules().stream().map(it -> it.getBasePackage());
|
||||
Stream<JavaPackage> extraPackages = extraIncludes.stream().map(Module::getBasePackage);
|
||||
|
||||
Stream<JavaPackage> intermediate = Stream.concat(moduleBasePackages, extraPackages);
|
||||
|
||||
return Stream.concat(intermediate, sharedBasePackages).distinct().collect(Collectors.toList());
|
||||
});
|
||||
|
||||
this.dependencies = Suppliers.memoize(() -> {
|
||||
|
||||
Stream<Module> bootstrapDependencies = module.getBootstrapDependencies(modules, bootstrapMode.getDepth());
|
||||
return Stream.concat(bootstrapDependencies, extraIncludes.stream()).collect(Collectors.toList());
|
||||
});
|
||||
|
||||
if (annotation.verifyAutomatically()) {
|
||||
verify();
|
||||
}
|
||||
}
|
||||
|
||||
public static java.util.function.Supplier<ModuleTestExecution> of(Class<?> type) {
|
||||
|
||||
return () -> {
|
||||
|
||||
ModuleTest annotation = AnnotatedElementUtils.findMergedAnnotation(type, ModuleTest.class);
|
||||
String packageName = type.getPackage().getName();
|
||||
|
||||
Class<?> modulithType = MODULITH_TYPES.computeIfAbsent(type,
|
||||
it -> new AnnotatedClassFinder(SpringBootApplication.class).findFromPackage(packageName));
|
||||
Modules modules = Modules.of(modulithType);
|
||||
Module module = modules.getModuleForPackage(packageName) //
|
||||
.orElseThrow(
|
||||
() -> new IllegalStateException(String.format("Package %s is not part of any module!", packageName)));
|
||||
|
||||
return EXECUTIONS.computeIfAbsent(Key.of(module.getBasePackage().getName(), annotation),
|
||||
it -> new ModuleTestExecution(annotation, modules, module));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all base packages the current execution needs to use for component scanning, auto-configuration etc.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Stream<String> getBasePackages() {
|
||||
return basePackages.get().stream().map(JavaPackage::getName);
|
||||
}
|
||||
|
||||
public boolean includes(String className) {
|
||||
|
||||
boolean result = modules.withinRootPackages(className) //
|
||||
|| basePackages.get().stream().anyMatch(it -> it.contains(className));
|
||||
|
||||
if (result) {
|
||||
LOG.debug("Including class {}.", className);
|
||||
}
|
||||
|
||||
return !result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all module dependencies, based on the current {@link BootstrapMode}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<Module> getDependencies() {
|
||||
return dependencies.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly trigger the module structure verification.
|
||||
*/
|
||||
public void verify() {
|
||||
modules.verify();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the setup of the module bootstrapped by this execution.
|
||||
*/
|
||||
public void verifyModule() {
|
||||
module.verifyDependencies(modules);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
*/
|
||||
@Override
|
||||
public Iterator<Module> iterator() {
|
||||
return modules.iterator();
|
||||
}
|
||||
|
||||
private static Stream<Module> getExtraModules(ModuleTest annotation, Modules modules) {
|
||||
|
||||
return Arrays.stream(annotation.extraIncludes()) //
|
||||
.map(modules::getModuleByName) //
|
||||
.flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty));
|
||||
}
|
||||
|
||||
@Value
|
||||
@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PRIVATE)
|
||||
private static class Key {
|
||||
|
||||
String moduleBasePackage;
|
||||
ModuleTest annotation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.test;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.boot.context.TypeExcludeFilter;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
class ModuleTypeExcludeFilter extends TypeExcludeFilter {
|
||||
|
||||
private final Supplier<ModuleTestExecution> execution;
|
||||
|
||||
public ModuleTypeExcludeFilter(Class<?> testClass) {
|
||||
this.execution = ModuleTestExecution.of(testClass);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.boot.context.TypeExcludeFilter#match(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory)
|
||||
*/
|
||||
@Override
|
||||
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
|
||||
return execution.get().includes(metadataReader.getClassMetadata().getClassName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2019-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* All Spring application events fired during the test execution.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public interface PublishedEvents {
|
||||
|
||||
/**
|
||||
* Creates a new {@link PublishedEvents} instance for the given events.
|
||||
*
|
||||
* @param events must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static PublishedEvents of(Object... events) {
|
||||
return of(Arrays.asList(events));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link PublishedEvents} instance for the given events.
|
||||
*
|
||||
* @param events must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static PublishedEvents of(Collection<? extends Object> events) {
|
||||
|
||||
Assert.notNull(events, "Events must not be null!");
|
||||
|
||||
return new DefaultPublishedEvents(events);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all application events of the given type that were fired during the test execution.
|
||||
*
|
||||
* @param <T> the event type
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
<T> TypedPublishedEvents<T> ofType(Class<T> type);
|
||||
|
||||
/**
|
||||
* All application events of a given type that were fired during a test execution.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @param <T> the event type
|
||||
*/
|
||||
interface TypedPublishedEvents<T> extends Iterable<T> {
|
||||
|
||||
/**
|
||||
* Further constrain the event type for downstream assertions.
|
||||
*
|
||||
* @param <S>
|
||||
* @param subType the sub type
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
<S extends T> TypedPublishedEvents<S> ofSubType(Class<S> subType);
|
||||
|
||||
/**
|
||||
* Returns all {@link TypedPublishedEvents} that match the given predicate.
|
||||
*
|
||||
* @param predicate must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
TypedPublishedEvents<T> matching(Predicate<? super T> predicate);
|
||||
|
||||
/**
|
||||
* Returns all {@link TypedPublishedEvents} that match the given predicate after applying the given mapping step.
|
||||
*
|
||||
* @param <S> the intermediate type to apply the {@link Predicate} on
|
||||
* @param mapper the mapping step to extract a part of the original event subject to test for the {@link Predicate}.
|
||||
* @param predicate the {@link Predicate} to apply on the value extracted.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
<S> TypedPublishedEvents<T> matchingMapped(Function<T, S> mapper, Predicate<? super S> predicate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.test;
|
||||
|
||||
import org.junit.jupiter.api.extension.Extension;
|
||||
|
||||
/**
|
||||
* JUnit 5 {@link Extension} for standalone usage without {@link ModuleTest}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public final class PublishedEventsExtension extends PublishedEventsParameterResolver {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.test;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||
import org.junit.jupiter.api.extension.BeforeAllCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
import org.junit.jupiter.api.extension.ParameterContext;
|
||||
import org.junit.jupiter.api.extension.ParameterResolver;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Provides instances of {@link PublishedEvents} as test method parameters.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class PublishedEventsParameterResolver implements ParameterResolver, BeforeAllCallback, AfterEachCallback {
|
||||
|
||||
private ThreadBoundApplicationListenerAdapter listener = new ThreadBoundApplicationListenerAdapter();
|
||||
private final Function<ExtensionContext, ApplicationContext> lookup;
|
||||
|
||||
PublishedEventsParameterResolver() {
|
||||
this(ctx -> SpringExtension.getApplicationContext(ctx));
|
||||
}
|
||||
|
||||
PublishedEventsParameterResolver(Function<ExtensionContext, ApplicationContext> supplier) {
|
||||
this.lookup = supplier;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.junit.jupiter.api.extension.BeforeAllCallback#beforeAll(org.junit.jupiter.api.extension.ExtensionContext)
|
||||
*/
|
||||
@Override
|
||||
public void beforeAll(ExtensionContext extensionContext) {
|
||||
|
||||
ApplicationContext context = lookup.apply(extensionContext);
|
||||
((ConfigurableApplicationContext) context).addApplicationListener(listener);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.junit.jupiter.api.extension.ParameterResolver#supportsParameter(org.junit.jupiter.api.extension.ParameterContext, org.junit.jupiter.api.extension.ExtensionContext)
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
|
||||
return PublishedEvents.class.isAssignableFrom(parameterContext.getParameter().getType());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.junit.jupiter.api.extension.ParameterResolver#resolveParameter(org.junit.jupiter.api.extension.ParameterContext, org.junit.jupiter.api.extension.ExtensionContext)
|
||||
*/
|
||||
@Override
|
||||
public PublishedEvents resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
|
||||
|
||||
DefaultPublishedEvents publishedEvents = new DefaultPublishedEvents();
|
||||
listener.registerDelegate(publishedEvents);
|
||||
|
||||
return publishedEvents;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.junit.jupiter.api.extension.AfterEachCallback#afterEach(org.junit.jupiter.api.extension.ExtensionContext)
|
||||
*/
|
||||
@Override
|
||||
public void afterEach(ExtensionContext context) {
|
||||
listener.unregisterDelegate();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ApplicationListener} that allows registering delegate {@link ApplicationListener}s that are held in a
|
||||
* {@link ThreadLocal} and get used on {@link #onApplicationEvent(ApplicationEvent)} if one is registered for the
|
||||
* current thread. This allows multiple event listeners to see the events fired in a certain thread in a concurrent
|
||||
* execution scenario.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
private static class ThreadBoundApplicationListenerAdapter implements ApplicationListener<ApplicationEvent> {
|
||||
|
||||
private final ThreadLocal<ApplicationListener<ApplicationEvent>> delegate = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* Registers the given {@link ApplicationListener} to be used for the current thread.
|
||||
*
|
||||
* @param listener must not be {@literal null}.
|
||||
*/
|
||||
void registerDelegate(ApplicationListener<ApplicationEvent> listener) {
|
||||
|
||||
Assert.notNull(listener, "Delegate ApplicationListener must not be null!");
|
||||
|
||||
delegate.set(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the registration of the currently assigned {@link ApplicationListener}.
|
||||
*/
|
||||
void unregisterDelegate() {
|
||||
delegate.remove();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
|
||||
*/
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
|
||||
ApplicationListener<ApplicationEvent> listener = delegate.get();
|
||||
|
||||
if (listener != null) {
|
||||
listener.onApplicationEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.UnsatisfiedDependencyException;
|
||||
import org.springframework.boot.test.context.SpringBootContextLoader;
|
||||
import org.springframework.boot.test.context.SpringBootTestContextBootstrapper;
|
||||
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.test.context.BootstrapContext;
|
||||
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
|
||||
import org.springframework.test.context.MergedContextConfiguration;
|
||||
import org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate;
|
||||
import org.springframework.test.context.support.DefaultBootstrapContext;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class TestUtils {
|
||||
|
||||
public static void assertDependencyMissing(Class<?> testClass, Class<?> expectedMissingDependency) {
|
||||
|
||||
CacheAwareContextLoaderDelegate delegate = new DefaultCacheAwareContextLoaderDelegate();
|
||||
BootstrapContext bootstrapContext = new DefaultBootstrapContext(testClass, delegate);
|
||||
|
||||
SpringBootTestContextBootstrapper bootstrapper = new SpringBootTestContextBootstrapper();
|
||||
bootstrapper.setBootstrapContext(bootstrapContext);
|
||||
|
||||
MergedContextConfiguration configuration = bootstrapper.buildMergedContextConfiguration();
|
||||
|
||||
AssertableApplicationContext context = AssertableApplicationContext.get(() -> {
|
||||
|
||||
SpringBootContextLoader loader = new SpringBootContextLoader();
|
||||
|
||||
try {
|
||||
|
||||
return (ConfigurableApplicationContext) loader.loadContext(configuration);
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
if (e instanceof RuntimeException) {
|
||||
throw (RuntimeException) e;
|
||||
}
|
||||
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
assertThat(context).hasFailed();
|
||||
|
||||
assertThat(context).getFailure().isInstanceOfSatisfying(UnsatisfiedDependencyException.class, it -> {
|
||||
assertThat(it.getMostSpecificCause()).isInstanceOfSatisfying(NoSuchBeanDefinitionException.class, ex -> {
|
||||
assertThat(ex.getBeanType()).isEqualTo(expectedMissingDependency);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.test.context.ContextCustomizerFactory=org.moduliths.test.ModuleContextCustomizerFactory
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ParameterContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Unit tests for PublishedEventsParameterResolver.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public class PublishedEventsParameterResolverUnitTests {
|
||||
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
|
||||
@Test
|
||||
void supportsPublishedEventsType() throws Exception {
|
||||
|
||||
PublishedEventsParameterResolver resolver = new PublishedEventsParameterResolver(__ -> context);
|
||||
|
||||
assertThat(resolver.supportsParameter(getParameterContext(PublishedEvents.class), null)).isTrue();
|
||||
assertThat(resolver.supportsParameter(getParameterContext(Object.class), null)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsThreadBoundPublishedEvents() throws Exception {
|
||||
|
||||
PublishedEventsParameterResolver resolver = new PublishedEventsParameterResolver(__ -> context);
|
||||
context.refresh();
|
||||
|
||||
resolver.beforeAll(null);
|
||||
|
||||
Map<String, PublishedEvents> allEvents = new ConcurrentHashMap<>();
|
||||
List<String> keys = Arrays.asList("first", "second", "third");
|
||||
CountDownLatch latch = new CountDownLatch(3);
|
||||
|
||||
for (String it : keys) {
|
||||
|
||||
new Thread(() -> {
|
||||
|
||||
PublishedEvents events = resolver.resolveParameter(null, null);
|
||||
context.publishEvent(it);
|
||||
allEvents.put(it, events);
|
||||
|
||||
resolver.afterEach(null);
|
||||
|
||||
latch.countDown();
|
||||
|
||||
}).start();
|
||||
|
||||
}
|
||||
|
||||
latch.await(50, TimeUnit.MILLISECONDS);
|
||||
|
||||
keys.forEach(it -> {
|
||||
assertThat(allEvents.get(it).ofType(String.class)).containsExactly(it);
|
||||
});
|
||||
}
|
||||
|
||||
private static ParameterContext getParameterContext(Class<?> type) {
|
||||
|
||||
Method method = ReflectionUtils.findMethod(Methods.class, "with", type);
|
||||
|
||||
ParameterContext context = mock(ParameterContext.class);
|
||||
doReturn(method.getParameters()[0]).when(context).getParameter();
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
interface Methods {
|
||||
|
||||
void with(PublishedEvents events);
|
||||
|
||||
void with(Object object);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link PublishedEvents}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class PublishedEventsUnitTests {
|
||||
|
||||
@Test
|
||||
void createsInstanceFromEvents() {
|
||||
|
||||
Object reference = new Object();
|
||||
|
||||
PublishedEvents events = PublishedEvents.of(reference);
|
||||
|
||||
assertThat(events.ofType(Object.class)).containsExactly(reference);
|
||||
}
|
||||
}
|
||||
16
moduliths-test/src/test/resources/logback.xml
Normal file
16
moduliths-test/src/test/resources/logback.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d %5p %40.40c:%4L - %m%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- <logger name="org.moduliths" level="debug" /> -->
|
||||
|
||||
<root level="error">
|
||||
<appender-ref ref="console" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user