问题描述
我想使用存根 bean(基本上什么都不做)运行我的大部分 Spring Boot 测试,但有些测试实际上应该使用真正的 Spring Boot 的自动配置 bean。如何在默认情况下在测试上下文中拥有存根 bean,但有条件地将其切换到真正的 bean 中?
我所有的测试都源自一些提供正确测试设置的超类,而这个特定的 bean 设置与其正交,所以我不能:
- 创建两个新的/不同的超类,一个用于存根,一个用于真实的超类
- 在测试类中使用
@MockBean
,因为大多数时候我想要存根,而不需要记住添加这个 - 在超类中使用
@MockBean
,因为我不知道如何在我想要 Spring Boot 自动配置模拟的特定测试类中覆盖模拟。
我正在寻找一个简单易用的解决方案,比如 JUnit-Rule;测试类上的注释以及 TestExecutionListener;或其他类似的东西。
请注意,真正的 bean 是一个完全自动配置的 bean,我的生产代码中没有对其进行任何配置(仅在 application.properties
中) - 它实际上是 JavaMailSender
但那个细节应该没关系。
解决方法
我会为此使用不同的配置文件。您可以使用 @ActiveProfiles("myprofile")
或您想用于该测试的任何配置文件来注释您的测试类。然后添加两个不同的 bean 配置并在其中指定配置文件 (@Profile("myprofile")
)。
不是很容易发现,深入到Spring TestContext Framework,但这是解决问题中的需求的解决方案:
在 ContextCustomizerFactory
中注册一个 META-INF/spring.factories
,这取决于当前测试类上的某个标记,要么注册一个 ContextCustomizer
,要么不注册。
ContextCustomizer
依次注册一个 BeanFactoryPostProcessor
,它从 BeanRegistry 中删除原始生产 bean 并添加模拟 bean。
所以它的工作原理与问题中的相反。默认情况下,它不会在上下文中包含模拟 bean,而是默认加载生产配置,最后在需要时将不需要的生产 bean 交换出去。
在测试资源中添加META-INF/spring.factories
:
org.springframework.test.context.ContextCustomizerFactory=my.package.MyContextCustomizerFactory
将这些类添加到您的测试源:
public class MyContextCustomizerFactory implements ContextCustomizerFactory {
@Override
public ContextCustomizer createContextCustomizer(
Class<?> testClass,List<ContextConfigurationAttributes> configAttributes) {
// Your logic to decide based on the testClass whether you want the real bean or the mock.
// See other ContextCustomizerFactories provided by Spring for how you can look at fields and annotations on the test class etc.
boolean shouldSwapBeanWithMock = ...
if (shouldSwapBeanWithMock) {
return new MyContextCustomizer();
} else {
return null;
}
}
private static class MyContextCustomizer implements ContextCustomizer {
/**
* ContextCustomizers are called very early in the application context lifecycle,where not all
* beans are registered yet. This is why we must register first a {@link
* BeanFactoryPostProcessor} which will do the bean swap,instead of doing it in the Customizer
* directly.
*/
@Override
public void customizeContext(
ConfigurableApplicationContext context,MergedContextConfiguration mergedConfig) {
context.addBeanFactoryPostProcessor(new MyBeanFactoryPostProcessor());
}
/**
* ContextCustomizers are part of the key which decides if an ApplicationContext can be
* retrieved from cache or needs to be freshly instantiated. {@code equals} and {@code hashCode}
* are overridden for that reason,because multiple instances of this Customizer are equivalent.
* The cache key should only depend on whether this customizer was present in the context or
* not.
*/
@Override
public boolean equals(Object obj) {
return (obj != null) && (obj.getClass() == getClass());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
private static class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// select whichever bean type you want to swap here
String[] beansToSwap = beanFactory.getBeanNamesForType(JavaMailSender.class,false,false);
Arrays.stream(beansToSwap)
.forEach(((BeanDefinitionRegistry) beanFactory)::removeBeanDefinition);
// register your mock bean as singleton or however you like
beanFactory.registerSingleton(MyMockMailSender.class.getName(),new MyMockMailSender());
}
}
}