Skip to content

GH-3679: Better caching for SpringIntegrationTest #3792

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2021 the original author or authors.
* Copyright 2017-2022 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.
Expand All @@ -16,20 +16,25 @@

package org.springframework.integration.test.context;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.BeansException;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.Lifecycle;
import org.springframework.context.SmartLifecycle;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.endpoint.IntegrationConsumer;
import org.springframework.integration.endpoint.ReactiveStreamsConsumer;
import org.springframework.integration.endpoint.SourcePollingChannelAdapter;
Expand Down Expand Up @@ -57,7 +62,7 @@
*
* @see SpringIntegrationTest
*/
public class MockIntegrationContext implements BeanFactoryAware {
public class MockIntegrationContext implements BeanPostProcessor, SmartInitializingSingleton, BeanFactoryAware {

private static final String HANDLER = "handler";

Expand All @@ -68,6 +73,8 @@ public class MockIntegrationContext implements BeanFactoryAware {

private final Map<String, Object> beans = new HashMap<>();

private final List<AbstractEndpoint> autoStartupCandidates = new ArrayList<>();

private ConfigurableListableBeanFactory beanFactory;

@Override
Expand All @@ -77,9 +84,35 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof AbstractEndpoint endpoint && endpoint.isAutoStartup()) {
addAutoStartupCandidates(endpoint);
}
return bean;
}

private void addAutoStartupCandidates(AbstractEndpoint endpoint) {
endpoint.setAutoStartup(false);
this.autoStartupCandidates.add(endpoint);
}

@Override
public void afterSingletonsInstantiated() {
this.beanFactory.getBeansOfType(AbstractEndpoint.class)
.values()
.stream()
.filter(AbstractEndpoint::isAutoStartup)
.forEach(this::addAutoStartupCandidates);
}

List<AbstractEndpoint> getAutoStartupCandidates() {
return this.autoStartupCandidates;
}

/**
* Reinstate the mocked beans after execution test to their real state.
* Typically is used from JUnit clean up method.
* Typically, this method is used from JUnit clean up methods.
* @param beanNames the bean names to reset.
* If {@code null}, all the mocked beans are reset
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2019 the original author or authors.
* Copyright 2017-2022 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.
Expand All @@ -16,10 +16,7 @@

package org.springframework.integration.test.context;

import java.beans.Introspector;

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ConfigurableApplicationContext;
Expand All @@ -30,20 +27,14 @@
/**
* The {@link ContextCustomizer} implementation for Spring Integration specific environment.
* <p>
* Registers {@link MockIntegrationContext}, {@link IntegrationEndpointsInitializer} beans.
* Registers {@link MockIntegrationContext} bean.
*
* @author Artem Bilan
*
* @since 5.0
*/
class MockIntegrationContextCustomizer implements ContextCustomizer {

private final SpringIntegrationTest springIntegrationTest;

MockIntegrationContextCustomizer(SpringIntegrationTest springIntegrationTest) {
this.springIntegrationTest = springIntegrationTest;
}

@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
Expand All @@ -52,29 +43,16 @@ public void customizeContext(ConfigurableApplicationContext context, MergedConte

registry.registerBeanDefinition(MockIntegrationContext.MOCK_INTEGRATION_CONTEXT_BEAN_NAME,
new RootBeanDefinition(MockIntegrationContext.class));

String endpointsInitializer = Introspector.decapitalize(IntegrationEndpointsInitializer.class.getSimpleName());

registry.registerBeanDefinition(endpointsInitializer,
BeanDefinitionBuilder.genericBeanDefinition(IntegrationEndpointsInitializer.class)
.addConstructorArgValue(this.springIntegrationTest)
.getBeanDefinition());

}

@Override
public int hashCode() {
return this.springIntegrationTest.hashCode();
return MockIntegrationContextCustomizer.class.hashCode();
}

@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != getClass()) {
return false;
}

MockIntegrationContextCustomizer customizer = (MockIntegrationContextCustomizer) obj;
return this.springIntegrationTest.equals(customizer.springIntegrationTest);
return obj != null && obj.getClass() == getClass();
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2019 the original author or authors.
* Copyright 2017-2022 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.
Expand Down Expand Up @@ -38,11 +38,8 @@ class MockIntegrationContextCustomizerFactory implements ContextCustomizerFactor
public ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {

SpringIntegrationTest springIntegrationTest =
AnnotatedElementUtils.findMergedAnnotation(testClass, SpringIntegrationTest.class);

return springIntegrationTest != null
? new MockIntegrationContextCustomizer(springIntegrationTest)
return AnnotatedElementUtils.hasAnnotation(testClass, SpringIntegrationTest.class)
? new MockIntegrationContextCustomizer()
: null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2019 the original author or authors.
* Copyright 2017-2022 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.
Expand All @@ -23,6 +23,8 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.test.context.TestExecutionListeners;

/**
* Annotation that can be specified on a test class that runs Spring Integration based tests.
* Provides the following features over and above the regular <em>Spring TestContext
Expand All @@ -32,15 +34,15 @@
* {@link MockIntegrationContext#MOCK_INTEGRATION_CONTEXT_BEAN_NAME} which can be used
* in tests for mocking and verifying integration flows.
* </li>
* <li>Registers an {@link IntegrationEndpointsInitializer} bean which is used
* <li>Registers an {@link SpringIntegrationTestExecutionListener} which is used
* to customize {@link org.springframework.integration.endpoint.AbstractEndpoint}
* beans with provided options on this annotation.
* beans with provided options on this annotation before/after the test class.
* </li>
* </ul>
* <p>
* The typical usage of this annotation is like:
* <pre class="code">
* &#064;RunWith(SpringRunner.class)
* &#064;SpringJUnitConfig
* &#064;SpringIntegrationTest
* public class MyIntegrationTests {
*
Expand All @@ -60,6 +62,8 @@
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@TestExecutionListeners(listeners = SpringIntegrationTestExecutionListener.class,
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
public @interface SpringIntegrationTest {

/**
Expand All @@ -68,9 +72,9 @@
* bean names to mark them as {@code autoStartup = false}
* during context initialization.
* @return the endpoints name patterns to stop during context initialization
* @see IntegrationEndpointsInitializer
* @see SpringIntegrationTestExecutionListener
* @see org.springframework.util.PatternMatchUtils
*/
String[] noAutoStartup() default {};
String[] noAutoStartup() default { };

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2017-2022 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 org.springframework.integration.test.context;

import java.util.Arrays;

import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.util.PatternMatchUtils;

/**
* A {@link TestExecutionListener} to customize {@link AbstractEndpoint} beans according
* to the provided options in the {@link SpringIntegrationTest} annotation
* before and after test class phases.
*
* @author Artem Bilan
*
* @since 5.0
*/
class SpringIntegrationTestExecutionListener implements TestExecutionListener {

@Override
public void beforeTestClass(TestContext testContext) {
SpringIntegrationTest springIntegrationTest =
AnnotatedElementUtils.findMergedAnnotation(testContext.getTestClass(), SpringIntegrationTest.class);

String[] patterns = springIntegrationTest.noAutoStartup();

ApplicationContext applicationContext = testContext.getApplicationContext();
MockIntegrationContext mockIntegrationContext = applicationContext.getBean(MockIntegrationContext.class);
mockIntegrationContext.getAutoStartupCandidates()
.stream()
.filter(endpoint -> !match(endpoint.getBeanName(), patterns))
.peek(endpoint -> endpoint.setAutoStartup(true))
.forEach(AbstractEndpoint::start);
}

@Override
public void afterTestClass(TestContext testContext) {
ApplicationContext applicationContext = testContext.getApplicationContext();
MockIntegrationContext mockIntegrationContext = applicationContext.getBean(MockIntegrationContext.class);
mockIntegrationContext.getAutoStartupCandidates()
.forEach(AbstractEndpoint::stop);
}

private boolean match(String name, String[] patterns) {
return patterns.length > 0 &&
Arrays.stream(patterns)
.anyMatch(pattern -> PatternMatchUtils.simpleMatch(pattern, name));
}

}
Loading