diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/context/IntegrationEndpointsInitializer.java b/spring-integration-test/src/main/java/org/springframework/integration/test/context/IntegrationEndpointsInitializer.java deleted file mode 100644 index 2ac6e9c7b64..00000000000 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/context/IntegrationEndpointsInitializer.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2017-2019 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.beans.BeansException; -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.integration.endpoint.AbstractEndpoint; -import org.springframework.util.Assert; -import org.springframework.util.PatternMatchUtils; - -/** - * A component to customize {@link AbstractEndpoint} beans according - * to the provided options in the {@link SpringIntegrationTest} annotation - * after all beans are registered in the application context but before its refresh. - *

- * This class implements both {@link SmartInitializingSingleton} and {@link BeanPostProcessor} - * to cover all the possible variants of {@link AbstractEndpoint} beans registration. - * First of all a bean for this class is registered in the application context, when XML configurations - * are parsed and registered already by the Spring Testing Framework, therefore a - * {@link BeanPostProcessor#postProcessBeforeInitialization(Object, String)} hook is not called for those beans. - * On the other hand we can't always rely on just a {@link SmartInitializingSingleton#afterSingletonsInstantiated()} - * because {@link SmartInitializingSingleton} beans are not ordered and some implementations may register beans - * later, than this {@link #afterSingletonsInstantiated()} is called. - * Plus beans might be registered at runtime, therefore {@link #postProcessBeforeInitialization(Object, String)} - * is still applied. - * - * @author Artem Bilan - * - * @since 5.0 - */ -class IntegrationEndpointsInitializer implements SmartInitializingSingleton, BeanPostProcessor, BeanFactoryAware { - - private final String[] patterns; - - private ConfigurableListableBeanFactory beanFactory; - - IntegrationEndpointsInitializer(SpringIntegrationTest springIntegrationTest) { - this.patterns = springIntegrationTest.noAutoStartup(); - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory); - this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; - } - - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof AbstractEndpoint && ((AbstractEndpoint) bean).isAutoStartup() && match(beanName)) { - ((AbstractEndpoint) bean).setAutoStartup(false); - } - return bean; - } - - @Override - public void afterSingletonsInstantiated() { - this.beanFactory.getBeansOfType(AbstractEndpoint.class) - .entrySet() - .stream() - .filter(entry -> entry.getValue().isAutoStartup() && match(entry.getKey())) - .forEach(entry -> entry.getValue().setAutoStartup(false)); - } - - private boolean match(String name) { - return Arrays.stream(this.patterns) - .anyMatch(pattern -> PatternMatchUtils.simpleMatch(pattern, name)); - } - -} diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContext.java b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContext.java index 199bd33763d..755467d7964 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContext.java +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContext.java @@ -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. @@ -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; @@ -57,7 +62,7 @@ * * @see SpringIntegrationTest */ -public class MockIntegrationContext implements BeanFactoryAware { +public class MockIntegrationContext implements BeanPostProcessor, SmartInitializingSingleton, BeanFactoryAware { private static final String HANDLER = "handler"; @@ -68,6 +73,8 @@ public class MockIntegrationContext implements BeanFactoryAware { private final Map beans = new HashMap<>(); + private final List autoStartupCandidates = new ArrayList<>(); + private ConfigurableListableBeanFactory beanFactory; @Override @@ -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 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 */ diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizer.java b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizer.java index 9ccd4b4d9a6..2e70daedbca 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizer.java +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizer.java @@ -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. @@ -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; @@ -30,7 +27,7 @@ /** * The {@link ContextCustomizer} implementation for Spring Integration specific environment. *

- * Registers {@link MockIntegrationContext}, {@link IntegrationEndpointsInitializer} beans. + * Registers {@link MockIntegrationContext} bean. * * @author Artem Bilan * @@ -38,12 +35,6 @@ */ 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(); @@ -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(); } } diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizerFactory.java b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizerFactory.java index fd9c28c8a96..a29d2eca965 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizerFactory.java +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizerFactory.java @@ -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. @@ -38,11 +38,8 @@ class MockIntegrationContextCustomizerFactory implements ContextCustomizerFactor public ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { - SpringIntegrationTest springIntegrationTest = - AnnotatedElementUtils.findMergedAnnotation(testClass, SpringIntegrationTest.class); - - return springIntegrationTest != null - ? new MockIntegrationContextCustomizer(springIntegrationTest) + return AnnotatedElementUtils.hasAnnotation(testClass, SpringIntegrationTest.class) + ? new MockIntegrationContextCustomizer() : null; } diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTest.java b/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTest.java index 248e0ea4515..8e7779cf5e1 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTest.java +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTest.java @@ -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. @@ -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 Spring TestContext @@ -32,15 +34,15 @@ * {@link MockIntegrationContext#MOCK_INTEGRATION_CONTEXT_BEAN_NAME} which can be used * in tests for mocking and verifying integration flows. * - *

  • Registers an {@link IntegrationEndpointsInitializer} bean which is used + *
  • 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. *
  • * *

    * The typical usage of this annotation is like: *

    - * @RunWith(SpringRunner.class)
    + * @SpringJUnitConfig
      * @SpringIntegrationTest
      * public class MyIntegrationTests {
      *
    @@ -60,6 +62,8 @@
     @Retention(RetentionPolicy.RUNTIME)
     @Documented
     @Inherited
    +@TestExecutionListeners(listeners = SpringIntegrationTestExecutionListener.class,
    +		mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
     public @interface SpringIntegrationTest {
     
     	/**
    @@ -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 { };
     
     }
    diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTestExecutionListener.java b/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTestExecutionListener.java
    new file mode 100644
    index 00000000000..1438d74481e
    --- /dev/null
    +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTestExecutionListener.java
    @@ -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));
    +	}
    +
    +}
    diff --git a/spring-integration-test/src/test/java/org/springframework/integration/test/mock/CachedSpringIntegrationTestAnnotationTests.java b/spring-integration-test/src/test/java/org/springframework/integration/test/mock/CachedSpringIntegrationTestAnnotationTests.java
    index 4b309098463..6d4d1d5ede7 100644
    --- a/spring-integration-test/src/test/java/org/springframework/integration/test/mock/CachedSpringIntegrationTestAnnotationTests.java
    +++ b/spring-integration-test/src/test/java/org/springframework/integration/test/mock/CachedSpringIntegrationTestAnnotationTests.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2018-2019 the original author or authors.
    + * Copyright 2018-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.
    @@ -20,9 +20,11 @@
     
     import org.junit.jupiter.api.Test;
     
    +import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
     import org.springframework.context.annotation.Bean;
     import org.springframework.context.annotation.Configuration;
    +import org.springframework.integration.endpoint.AbstractEndpoint;
     import org.springframework.integration.test.context.SpringIntegrationTest;
     import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
     
    @@ -37,22 +39,28 @@ public abstract class CachedSpringIntegrationTestAnnotationTests {
     
     	private static int beanFactoryPostProcessorCallCounter;
     
    +	@Autowired
    +	protected AbstractEndpoint someEndpoint;
    +
     	public static class SpringIntegrationTestAnnotationCaching1Tests
     			extends CachedSpringIntegrationTestAnnotationTests {
     
     		@Test
     		public void testSingleApplicationContext() {
     			assertThat(beanFactoryPostProcessorCallCounter).isEqualTo(1);
    +			assertThat(this.someEndpoint.isRunning()).isTrue();
     		}
     
     	}
     
    +	@SpringIntegrationTest(noAutoStartup = "someEndpoint")
     	public static class SpringIntegrationTestAnnotationCaching2Tests
     			extends CachedSpringIntegrationTestAnnotationTests {
     
     		@Test
     		public void testSingleApplicationContext() {
     			assertThat(beanFactoryPostProcessorCallCounter).isEqualTo(1);
    +			assertThat(this.someEndpoint.isRunning()).isFalse();
     		}
     
     	}
    @@ -65,6 +73,23 @@ public static BeanFactoryPostProcessor tesBeanFactoryPostProcessor() {
     			return beanFactory -> beanFactoryPostProcessorCallCounter++;
     		}
     
    +		@Bean
    +		public AbstractEndpoint someEndpoint() {
    +			return new AbstractEndpoint() {
    +
    +				@Override
    +				protected void doStart() {
    +
    +				}
    +
    +				@Override
    +				protected void doStop() {
    +
    +				}
    +
    +			};
    +		}
    +
     	}
     
     }
    diff --git a/spring-integration-test/src/test/java/org/springframework/integration/test/mock/MockMessageSourceTests.java b/spring-integration-test/src/test/java/org/springframework/integration/test/mock/MockMessageSourceTests.java
    index 8c2424b4be7..8aff663bb21 100644
    --- a/spring-integration-test/src/test/java/org/springframework/integration/test/mock/MockMessageSourceTests.java
    +++ b/spring-integration-test/src/test/java/org/springframework/integration/test/mock/MockMessageSourceTests.java
    @@ -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.
    @@ -19,9 +19,8 @@
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.fail;
     
    -import org.junit.After;
    -import org.junit.Test;
    -import org.junit.runner.RunWith;
    +import org.junit.jupiter.api.AfterEach;
    +import org.junit.jupiter.api.Test;
     
     import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -47,16 +46,14 @@
     import org.springframework.messaging.Message;
     import org.springframework.messaging.support.GenericMessage;
     import org.springframework.test.annotation.DirtiesContext;
    -import org.springframework.test.context.ContextConfiguration;
    -import org.springframework.test.context.junit4.SpringRunner;
    +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
     
     /**
      * @author Artem Bilan
      *
      * @since 5.0
      */
    -@RunWith(SpringRunner.class)
    -@ContextConfiguration(classes = MockMessageSourceTests.Config.class)
    +@SpringJUnitConfig(classes = MockMessageSourceTests.Config.class)
     @SpringIntegrationTest(noAutoStartup = { "inboundChannelAdapter", "*Source*" })
     @DirtiesContext
     public class MockMessageSourceTests {
    @@ -73,7 +70,7 @@ public class MockMessageSourceTests {
     	@Autowired
     	private IntegrationFlowContext integrationFlowContext;
     
    -	@After
    +	@AfterEach
     	public void tearDown() {
     		this.mockIntegrationContext.resetBeans("mySourceEndpoint", "inboundChannelAdapter");
     		this.results.purge(null);
    diff --git a/src/reference/asciidoc/testing.adoc b/src/reference/asciidoc/testing.adoc
    index 88f6accbdeb..66758f9e37b 100644
    --- a/src/reference/asciidoc/testing.adoc
    +++ b/src/reference/asciidoc/testing.adoc
    @@ -196,7 +196,7 @@ First we configure our test class with a `@SpringIntegrationTest` annotation to
     ====
     [source,java]
     ----
    -@RunWith(SpringRunner.class)
    +@SpringJUnitConfig
     @SpringIntegrationTest(noAutoStartup = {"inboundChannelAdapter", "*Source*"})
     public class MyIntegrationTests {