Skip to content

Commit

Permalink
GH-320 - Explicitly drop non-bootstrapped module beans during test run.
Browse files Browse the repository at this point in the history
We now explicitly drop all beans resulting in a type that's contained in an application module *not* included in the current test bootstrap.
  • Loading branch information
odrotbohm committed Oct 13, 2023
1 parent 61fa94a commit c8b81e0
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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 {}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,19 @@

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
Expand All @@ -53,7 +59,6 @@ public ContextCustomizer createContextCustomizer(Class<?> testClass,
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;

Expand All @@ -73,7 +78,9 @@ public void customizeContext(ConfigurableApplicationContext context, MergedConte
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);
Expand Down Expand Up @@ -173,4 +180,78 @@ public int hashCode() {
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 {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;
Expand Down Expand Up @@ -227,7 +228,7 @@ private static Stream<ApplicationModule> getExtraModules(ApplicationModuleTest a

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) {}
Expand Down

0 comments on commit c8b81e0

Please sign in to comment.