GH-320 - Explicitly drop non-bootstrapped module beans during test run.

We now explicitly drop all beans resulting in a type that's contained in an application module *not* included in the current test bootstrap.
This commit is contained in:
Oliver Drotbohm
2023-10-13 23:02:40 +02:00
parent 61fa94ad63
commit c8b81e0737
5 changed files with 182 additions and 3 deletions

View File

@@ -24,13 +24,19 @@ import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.modulith.core.ApplicationModule;
import org.springframework.modulith.core.JavaPackage;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.Assert;
/**
* @author Oliver Drotbohm
@@ -53,7 +59,6 @@ class ModuleContextCustomizerFactory implements ContextCustomizerFactory {
static class ModuleContextCustomizer implements ContextCustomizer {
private static final Logger LOGGER = LoggerFactory.getLogger(ModuleContextCustomizer.class);
private static final String BEAN_NAME = ModuleTestExecution.class.getName();
private final Supplier<ModuleTestExecution> execution;
@@ -73,7 +78,9 @@ class ModuleContextCustomizerFactory implements ContextCustomizerFactory {
logModules(testExecution);
var beanFactory = context.getBeanFactory();
beanFactory.registerSingleton(BEAN_NAME, testExecution);
beanFactory.registerSingleton(ModuleTestExecution.class.getName(), testExecution);
beanFactory.registerSingleton(ModuleTestExecutionBeanDefinitionSelector.class.getName(),
new ModuleTestExecutionBeanDefinitionSelector(testExecution));
var events = new DefaultPublishedEvents();
beanFactory.registerSingleton(events.getClass().getName(), events);
@@ -173,4 +180,78 @@ class ModuleContextCustomizerFactory implements ContextCustomizerFactory {
return Objects.hash(execution);
}
}
/**
* A {@link BeanDefinitionRegistryPostProcessor} that selects
* {@link org.springframework.beans.factory.config.BeanDefinition}s that are either non-module beans (i.e.
* infrastructure) or beans living inside an {@link ApplicationModule} being part of the current
* {@link ModuleTestExecution}.
*
* @author Oliver Drotbohm
* @since 1.1
*/
private static class ModuleTestExecutionBeanDefinitionSelector implements BeanDefinitionRegistryPostProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(ModuleTestExecutionBeanDefinitionSelector.class);
private final ModuleTestExecution execution;
/**
* Creates a new {@link ModuleTestExecutionBeanDefinitionSelector} for the given {@link ModuleTestExecution}.
*
* @param execution must not be {@literal null}.
*/
private ModuleTestExecutionBeanDefinitionSelector(ModuleTestExecution execution) {
Assert.notNull(execution, "ModuleTestExecution must not be null!");
this.execution = execution;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry(org.springframework.beans.factory.support.BeanDefinitionRegistry)
*/
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (!(registry instanceof ConfigurableListableBeanFactory factory)) {
return;
}
var modules = execution.getModules();
for (String name : registry.getBeanDefinitionNames()) {
var type = factory.getType(name, false);
var module = modules.getModuleByType(type);
// Not a module type -> pass
if (module.isEmpty()) {
continue;
}
var packagesIncludedInTestRun = execution.getBasePackages().toList();
// A type of a module bootstrapped -> pass
if (module.map(ApplicationModule::getBasePackage)
.map(JavaPackage::getName)
.filter(packagesIncludedInTestRun::contains).isPresent()) {
continue;
}
LOGGER.trace("Dropping bean definition {} for type {} as it is not included in an application module to be bootstrapped!", name, type.getName());
// Remove bean definition from bootstrap
registry.removeBeanDefinition(name);
}
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.config.BeanFactoryPostProcessor#postProcessBeanFactory(org.springframework.beans.factory.config.ConfigurableListableBeanFactory)
*/
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}
}
}

View File

@@ -21,6 +21,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import org.slf4j.Logger;
@@ -227,7 +228,7 @@ public class ModuleTestExecution implements Iterable<ApplicationModule> {
return Arrays.stream(annotation.extraIncludes()) //
.map(modules::getModuleByName) //
.flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty));
.flatMap(Optional::stream);
}
private static record Key(String moduleBasePackage, ApplicationModuleTest annotation) {}