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:
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2018-2023 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 com.acme.myproject.moduleD;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.acme.myproject.moduleB.ServiceComponentB;
|
||||
import com.acme.myproject.moduleE.ServiceComponentE;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Configuration
|
||||
class SomeConfigurationD {
|
||||
|
||||
@Bean
|
||||
ServiceComponentE serviceComponentE() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2018-2023 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 com.acme.myproject.moduleE;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Component
|
||||
public class ServiceComponentE {}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2023 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 com.acme.myproject.moduleD;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
import com.acme.myproject.NonVerifyingModuleTest;
|
||||
import com.acme.myproject.moduleE.ServiceComponentE;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@NonVerifyingModuleTest
|
||||
class ModuleDTest {
|
||||
|
||||
@Autowired ConfigurableApplicationContext context;
|
||||
|
||||
@Test // GH-320
|
||||
void dropsManuallyDeclaredBeanOfNonIncludedModule() {
|
||||
assertThat(context.getBeanNamesForType(ServiceComponentE.class)).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
Reference in New Issue
Block a user