Skip to content

Commit c8b81e0

Browse files
committed
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.
1 parent 61fa94a commit c8b81e0

File tree

5 files changed

+182
-3
lines changed

5 files changed

+182
-3
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2018-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.acme.myproject.moduleD;
17+
18+
import org.springframework.context.annotation.Bean;
19+
import org.springframework.context.annotation.Configuration;
20+
21+
import com.acme.myproject.moduleB.ServiceComponentB;
22+
import com.acme.myproject.moduleE.ServiceComponentE;
23+
24+
/**
25+
* @author Oliver Drotbohm
26+
*/
27+
@Configuration
28+
class SomeConfigurationD {
29+
30+
@Bean
31+
ServiceComponentE serviceComponentE() {
32+
return null;
33+
}
34+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2018-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.acme.myproject.moduleE;
17+
18+
import org.springframework.stereotype.Component;
19+
20+
/**
21+
* @author Oliver Drotbohm
22+
*/
23+
@Component
24+
public class ServiceComponentE {}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.acme.myproject.moduleD;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.jupiter.api.Test;
21+
import org.springframework.beans.factory.annotation.Autowired;
22+
import org.springframework.context.ConfigurableApplicationContext;
23+
24+
import com.acme.myproject.NonVerifyingModuleTest;
25+
import com.acme.myproject.moduleE.ServiceComponentE;
26+
27+
/**
28+
* @author Oliver Drotbohm
29+
*/
30+
@NonVerifyingModuleTest
31+
class ModuleDTest {
32+
33+
@Autowired ConfigurableApplicationContext context;
34+
35+
@Test // GH-320
36+
void dropsManuallyDeclaredBeanOfNonIncludedModule() {
37+
assertThat(context.getBeanNamesForType(ServiceComponentE.class)).isEmpty();
38+
}
39+
}

spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleContextCustomizerFactory.java

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,19 @@
2424

2525
import org.slf4j.Logger;
2626
import org.slf4j.LoggerFactory;
27+
import org.springframework.beans.BeansException;
28+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
29+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
30+
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
2731
import org.springframework.context.ConfigurableApplicationContext;
2832
import org.springframework.modulith.core.ApplicationModule;
33+
import org.springframework.modulith.core.JavaPackage;
2934
import org.springframework.test.context.ContextConfigurationAttributes;
3035
import org.springframework.test.context.ContextCustomizer;
3136
import org.springframework.test.context.ContextCustomizerFactory;
3237
import org.springframework.test.context.MergedContextConfiguration;
3338
import org.springframework.test.context.TestContextAnnotationUtils;
39+
import org.springframework.util.Assert;
3440

3541
/**
3642
* @author Oliver Drotbohm
@@ -53,7 +59,6 @@ public ContextCustomizer createContextCustomizer(Class<?> testClass,
5359
static class ModuleContextCustomizer implements ContextCustomizer {
5460

5561
private static final Logger LOGGER = LoggerFactory.getLogger(ModuleContextCustomizer.class);
56-
private static final String BEAN_NAME = ModuleTestExecution.class.getName();
5762

5863
private final Supplier<ModuleTestExecution> execution;
5964

@@ -73,7 +78,9 @@ public void customizeContext(ConfigurableApplicationContext context, MergedConte
7378
logModules(testExecution);
7479

7580
var beanFactory = context.getBeanFactory();
76-
beanFactory.registerSingleton(BEAN_NAME, testExecution);
81+
beanFactory.registerSingleton(ModuleTestExecution.class.getName(), testExecution);
82+
beanFactory.registerSingleton(ModuleTestExecutionBeanDefinitionSelector.class.getName(),
83+
new ModuleTestExecutionBeanDefinitionSelector(testExecution));
7784

7885
var events = new DefaultPublishedEvents();
7986
beanFactory.registerSingleton(events.getClass().getName(), events);
@@ -173,4 +180,78 @@ public int hashCode() {
173180
return Objects.hash(execution);
174181
}
175182
}
183+
184+
/**
185+
* A {@link BeanDefinitionRegistryPostProcessor} that selects
186+
* {@link org.springframework.beans.factory.config.BeanDefinition}s that are either non-module beans (i.e.
187+
* infrastructure) or beans living inside an {@link ApplicationModule} being part of the current
188+
* {@link ModuleTestExecution}.
189+
*
190+
* @author Oliver Drotbohm
191+
* @since 1.1
192+
*/
193+
private static class ModuleTestExecutionBeanDefinitionSelector implements BeanDefinitionRegistryPostProcessor {
194+
195+
private static final Logger LOGGER = LoggerFactory.getLogger(ModuleTestExecutionBeanDefinitionSelector.class);
196+
197+
private final ModuleTestExecution execution;
198+
199+
/**
200+
* Creates a new {@link ModuleTestExecutionBeanDefinitionSelector} for the given {@link ModuleTestExecution}.
201+
*
202+
* @param execution must not be {@literal null}.
203+
*/
204+
private ModuleTestExecutionBeanDefinitionSelector(ModuleTestExecution execution) {
205+
206+
Assert.notNull(execution, "ModuleTestExecution must not be null!");
207+
208+
this.execution = execution;
209+
}
210+
211+
/*
212+
* (non-Javadoc)
213+
* @see org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry(org.springframework.beans.factory.support.BeanDefinitionRegistry)
214+
*/
215+
@Override
216+
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
217+
218+
if (!(registry instanceof ConfigurableListableBeanFactory factory)) {
219+
return;
220+
}
221+
222+
var modules = execution.getModules();
223+
224+
for (String name : registry.getBeanDefinitionNames()) {
225+
226+
var type = factory.getType(name, false);
227+
var module = modules.getModuleByType(type);
228+
229+
// Not a module type -> pass
230+
if (module.isEmpty()) {
231+
continue;
232+
}
233+
234+
var packagesIncludedInTestRun = execution.getBasePackages().toList();
235+
236+
// A type of a module bootstrapped -> pass
237+
if (module.map(ApplicationModule::getBasePackage)
238+
.map(JavaPackage::getName)
239+
.filter(packagesIncludedInTestRun::contains).isPresent()) {
240+
continue;
241+
}
242+
243+
LOGGER.trace("Dropping bean definition {} for type {} as it is not included in an application module to be bootstrapped!", name, type.getName());
244+
245+
// Remove bean definition from bootstrap
246+
registry.removeBeanDefinition(name);
247+
}
248+
}
249+
250+
/*
251+
* (non-Javadoc)
252+
* @see org.springframework.beans.factory.config.BeanFactoryPostProcessor#postProcessBeanFactory(org.springframework.beans.factory.config.ConfigurableListableBeanFactory)
253+
*/
254+
@Override
255+
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}
256+
}
176257
}

spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222
import java.util.Map;
2323
import java.util.Objects;
24+
import java.util.Optional;
2425
import java.util.stream.Stream;
2526

2627
import org.slf4j.Logger;
@@ -227,7 +228,7 @@ private static Stream<ApplicationModule> getExtraModules(ApplicationModuleTest a
227228

228229
return Arrays.stream(annotation.extraIncludes()) //
229230
.map(modules::getModuleByName) //
230-
.flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty));
231+
.flatMap(Optional::stream);
231232
}
232233

233234
private static record Key(String moduleBasePackage, ApplicationModuleTest annotation) {}

0 commit comments

Comments
 (0)