diff --git a/build.gradle b/build.gradle index e9c3eaade5..61c2a58ddf 100644 --- a/build.gradle +++ b/build.gradle @@ -157,8 +157,8 @@ subprojects { subproject -> // dependencies that are common across all java projects dependencies { - if (!(subproject.name ==~ /.*-test/)) { - testCompile project(":spring-integration-test") + if (!(subproject.name ==~ /.*-test.*/)) { + testCompile project(":spring-integration-test-support") } testRuntime "org.slf4j:slf4j-log4j12:$slf4jVersion" @@ -256,7 +256,7 @@ subprojects { subproject -> build.dependsOn jacocoTestReport } -project('spring-integration-test') { +project('spring-integration-test-support') { description = 'Spring Integration Test Support - **No SI Dependencies Allowed**' dependencies { compile ("junit:junit:$junitVersion") { @@ -612,6 +612,14 @@ project('spring-integration-syslog') { } } +project('spring-integration-test') { + description = 'Spring Integration Testing Framework' + dependencies { + compile project(":spring-integration-core") + compile project(":spring-integration-test-support") + } +} + project('spring-integration-twitter') { description = 'Spring Integration Twitter Support' dependencies { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/channel/MixedDispatcherConfigurationScenarioTests.java b/spring-integration-core/src/test/java/org/springframework/integration/channel/MixedDispatcherConfigurationScenarioTests.java index 695ec7b02e..8f07a27654 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/channel/MixedDispatcherConfigurationScenarioTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/channel/MixedDispatcherConfigurationScenarioTests.java @@ -151,10 +151,10 @@ public class MixedDispatcherConfigurationScenarioTests { executor.execute(messageSenderTask); } start.countDown(); - assertTrue(allDone.await(5, TimeUnit.SECONDS)); + assertTrue(allDone.await(10, TimeUnit.SECONDS)); executor.shutdown(); - executor.awaitTermination(5, TimeUnit.SECONDS); + executor.awaitTermination(10, TimeUnit.SECONDS); assertTrue("not all messages were accepted", failed.get()); verify(handlerA, times(TOTAL_EXECUTIONS)).handleMessage(message); @@ -192,10 +192,10 @@ public class MixedDispatcherConfigurationScenarioTests { executor.execute(messageSenderTask); } start.countDown(); - assertTrue(allDone.await(5, TimeUnit.SECONDS)); + assertTrue(allDone.await(10, TimeUnit.SECONDS)); executor.shutdown(); - executor.awaitTermination(5, TimeUnit.SECONDS); + executor.awaitTermination(10, TimeUnit.SECONDS); assertTrue("not all messages were accepted", failed.get()); verify(handlerA, times(TOTAL_EXECUTIONS)).handleMessage(message); @@ -273,10 +273,10 @@ public class MixedDispatcherConfigurationScenarioTests { executor.execute(messageSenderTask); } start.countDown(); - assertTrue(allDone.await(5, TimeUnit.SECONDS)); + assertTrue(allDone.await(10, TimeUnit.SECONDS)); executor.shutdown(); - executor.awaitTermination(5, TimeUnit.SECONDS); + executor.awaitTermination(10, TimeUnit.SECONDS); assertTrue("not all messages were accepted", failed.get()); verify(handlerA, times(14)).handleMessage(message); @@ -407,10 +407,10 @@ public class MixedDispatcherConfigurationScenarioTests { executor.execute(messageSenderTask); } start.countDown(); - assertTrue(allDone.await(5, TimeUnit.SECONDS)); + assertTrue(allDone.await(10, TimeUnit.SECONDS)); executor.shutdown(); - executor.awaitTermination(5, TimeUnit.SECONDS); + executor.awaitTermination(10, TimeUnit.SECONDS); assertFalse("not all messages were accepted", failed.get()); verify(handlerA, times(TOTAL_EXECUTIONS)).handleMessage(message); @@ -451,10 +451,10 @@ public class MixedDispatcherConfigurationScenarioTests { } start.countDown(); - assertTrue(allDone.await(5, TimeUnit.SECONDS)); + assertTrue(allDone.await(10, TimeUnit.SECONDS)); executor.shutdown(); - executor.awaitTermination(5, TimeUnit.SECONDS); + executor.awaitTermination(10, TimeUnit.SECONDS); verify(handlerA, times(TOTAL_EXECUTIONS)).handleMessage(message); verify(handlerB, times(TOTAL_EXECUTIONS)).handleMessage(message); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/channel/config/ThreadLocalChannelParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/channel/config/ThreadLocalChannelParserTests.java index 8f9c7b4e73..896f2ba6b8 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/channel/config/ThreadLocalChannelParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/channel/config/ThreadLocalChannelParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 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. @@ -18,6 +18,7 @@ package org.springframework.integration.channel.config; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.List; @@ -34,15 +35,18 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.integration.config.TestChannelInterceptor; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * @author Mark Fisher * @author Dave Syer + * @author Artem Bilan */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration +@DirtiesContext public class ThreadLocalChannelParserTests { @Autowired @Qualifier("simpleChannel") @@ -63,7 +67,7 @@ public class ThreadLocalChannelParserTests { simpleChannel.send(new GenericMessage("crap")); latch.countDown(); }); - latch.await(1, TimeUnit.SECONDS); + assertTrue(latch.await(10, TimeUnit.SECONDS)); assertEquals("test", simpleChannel.receive(10).getPayload()); // Message sent on another thread is not collected here assertEquals(null, simpleChannel.receive(10)); @@ -87,7 +91,7 @@ public class ThreadLocalChannelParserTests { otherThreadResults.add(channelWithInterceptor.receive(0)); latch.countDown(); }); - latch.await(1, TimeUnit.SECONDS); + assertTrue(latch.await(10, TimeUnit.SECONDS)); assertEquals(2, otherThreadResults.size()); assertNull(otherThreadResults.get(0)); assertNull(otherThreadResults.get(1)); diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/JdbcMessageStoreChannelOnePollerIntegrationTests.java b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/JdbcMessageStoreChannelOnePollerIntegrationTests.java index a0f1629513..453e249c40 100644 --- a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/JdbcMessageStoreChannelOnePollerIntegrationTests.java +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/JdbcMessageStoreChannelOnePollerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 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. @@ -86,7 +86,7 @@ public class JdbcMessageStoreChannelOnePollerIntegrationTests { boolean result1 = relay.send(new GenericMessage("foo"), 500L); // This will time out because the transaction has not committed yet try { - Service.await(1000); + Service.await(10000); fail("Expected timeout"); } catch (Exception e) { @@ -112,7 +112,7 @@ public class JdbcMessageStoreChannelOnePollerIntegrationTests { // If the poll blocks in the RDBMS there is no way for the queue to respect the timeout assertTrue("Timed out waiting for receive", stopWatch.getTotalTimeMillis() < 10000); - Service.await(1000); + Service.await(10000); // Eventual activation assertEquals(1, Service.messages.size()); diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/mail/TestMailServer.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/mail/TestMailServer.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/mail/TestMailServer.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/mail/TestMailServer.java diff --git a/spring-integration-test-support/src/main/java/org/springframework/integration/test/mail/package-info.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/mail/package-info.java new file mode 100644 index 0000000000..703ec87344 --- /dev/null +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/mail/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides testing utils for mail support. + */ +package org.springframework.integration.test.mail; diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/EqualsResultMatcher.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/EqualsResultMatcher.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/matcher/EqualsResultMatcher.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/EqualsResultMatcher.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/EventuallyMatcher.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/EventuallyMatcher.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/matcher/EventuallyMatcher.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/EventuallyMatcher.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/HeaderMatcher.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/HeaderMatcher.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/matcher/HeaderMatcher.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/HeaderMatcher.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/MapContentMatchers.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/MapContentMatchers.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/matcher/MapContentMatchers.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/MapContentMatchers.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/MockitoMessageMatchers.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/MockitoMessageMatchers.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/matcher/MockitoMessageMatchers.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/MockitoMessageMatchers.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/PayloadAndHeaderMatcher.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/PayloadAndHeaderMatcher.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/matcher/PayloadAndHeaderMatcher.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/PayloadAndHeaderMatcher.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/PayloadMatcher.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/PayloadMatcher.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/matcher/PayloadMatcher.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/PayloadMatcher.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/TypeSafeMatcher.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/TypeSafeMatcher.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/matcher/TypeSafeMatcher.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/TypeSafeMatcher.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/matcher/package-info.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/package-info.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/matcher/package-info.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/package-info.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/rule/Log4jLevelAdjuster.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/rule/Log4jLevelAdjuster.java old mode 100755 new mode 100644 similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/rule/Log4jLevelAdjuster.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/rule/Log4jLevelAdjuster.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/rule/package-info.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/rule/package-info.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/rule/package-info.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/rule/package-info.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/support/AbstractRequestResponseScenarioTests.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/AbstractRequestResponseScenarioTests.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/support/AbstractRequestResponseScenarioTests.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/support/AbstractRequestResponseScenarioTests.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/support/AbstractResponseValidator.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/AbstractResponseValidator.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/support/AbstractResponseValidator.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/support/AbstractResponseValidator.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/support/LogAdjustingTestSupport.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/LogAdjustingTestSupport.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/support/LogAdjustingTestSupport.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/support/LogAdjustingTestSupport.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/support/LongRunningIntegrationTest.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/LongRunningIntegrationTest.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/support/LongRunningIntegrationTest.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/support/LongRunningIntegrationTest.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/support/MessageValidator.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/MessageValidator.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/support/MessageValidator.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/support/MessageValidator.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/support/PayloadValidator.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/PayloadValidator.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/support/PayloadValidator.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/support/PayloadValidator.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/support/RequestResponseScenario.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/RequestResponseScenario.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/support/RequestResponseScenario.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/support/RequestResponseScenario.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/support/SingleRequestResponseScenarioTests.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/SingleRequestResponseScenarioTests.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/support/SingleRequestResponseScenarioTests.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/support/SingleRequestResponseScenarioTests.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/support/package-info.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/package-info.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/support/package-info.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/support/package-info.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/util/HeapDumper.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/util/HeapDumper.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/util/HeapDumper.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/util/HeapDumper.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/util/OnlyOnceTrigger.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/util/OnlyOnceTrigger.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/util/OnlyOnceTrigger.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/util/OnlyOnceTrigger.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/util/SocketUtils.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/util/SocketUtils.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/util/SocketUtils.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/util/SocketUtils.java diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/util/TestUtils.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/util/TestUtils.java similarity index 87% rename from spring-integration-test/src/main/java/org/springframework/integration/test/util/TestUtils.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/util/TestUtils.java index 0d4ccd7aae..9c2b69de20 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/util/TestUtils.java +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/util/TestUtils.java @@ -56,6 +56,15 @@ import org.springframework.util.StringUtils; */ public abstract class TestUtils { + /** + * Obtain a value for the property from the provide object. + * Supports nested properties via period delimiter. + * @param root the object to obtain the property value + * @param propertyPath the property name to obtain a value. + * Can be nested path defined by the period. + * @return the value of the property or null + * @see DirectFieldAccessor + */ public static Object getPropertyValue(Object root, String propertyPath) { Object value = null; DirectFieldAccessor accessor = new DirectFieldAccessor(root); @@ -76,6 +85,18 @@ public abstract class TestUtils { return value; } + /** + * Obtain a value for the property from the provide object + * and try to cast it to the provided type. + * Supports nested properties via period delimiter. + * @param root the object to obtain the property value + * @param propertyPath the property name to obtain a value. + * @param type the expected value type. + * @param the expected value type. + * Can be nested path defined by the period. + * @return the value of the property or null + * @see DirectFieldAccessor + */ @SuppressWarnings("unchecked") public static T getPropertyValue(Object root, String propertyPath, Class type) { Object value = getPropertyValue(root, propertyPath); @@ -85,6 +106,11 @@ public abstract class TestUtils { return (T) value; } + /** + * Create a {@link TestApplicationContext} instance + * supplied with the basic Spring Integration infrastructure. + * @return the {@link TestApplicationContext} instance + */ public static TestApplicationContext createTestApplicationContext() { TestApplicationContext context = new TestApplicationContext(); ErrorHandler errorHandler = new MessagePublishingErrorHandler(context); @@ -95,6 +121,11 @@ public abstract class TestUtils { return context; } + /** + * A factory for the {@link ThreadPoolTaskScheduler} instances based on the provided {@code poolSize}. + * @param poolSize the size for the {@link ThreadPoolTaskScheduler} + * @return the {@link ThreadPoolTaskScheduler} instance. + */ public static ThreadPoolTaskScheduler createTaskScheduler(int poolSize) { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(poolSize); @@ -130,6 +161,10 @@ public abstract class TestUtils { } + /** + * A {@link GenericApplicationContext} extension with some support methods + * to register Spring Integration beans in the application context at runtime. + */ public static class TestApplicationContext extends GenericApplicationContext { TestApplicationContext() { @@ -150,6 +185,14 @@ public abstract class TestUtils { TestUtils.registerBean(channelName, channel, this); } + public void registerEndpoint(String endpointName, Object endpoint) { + TestUtils.registerBean(endpointName, endpoint, this); + } + + public void registerBean(String beanName, Object bean) { + TestUtils.registerBean(beanName, bean, this); + } + private String getComponentNameIfNamed(final MessageChannel channel) { Set> interfaces = ClassUtils.getAllInterfacesAsSet(channel); final AtomicReference componentName = new AtomicReference(); @@ -169,14 +212,6 @@ public abstract class TestUtils { return componentName.get(); } - public void registerEndpoint(String endpointName, Object endpoint) { - TestUtils.registerBean(endpointName, endpoint, this); - } - - public void registerBean(String beanName, Object bean) { - TestUtils.registerBean(beanName, bean, this); - } - } /** diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/util/package-info.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/util/package-info.java similarity index 100% rename from spring-integration-test/src/main/java/org/springframework/integration/test/util/package-info.java rename to spring-integration-test-support/src/main/java/org/springframework/integration/test/util/package-info.java diff --git a/spring-integration-test/src/test/java/org/springframework/integration/test/matcher/MapContainsTests.java b/spring-integration-test-support/src/test/java/org/springframework/integration/test/matcher/MapContainsTests.java similarity index 100% rename from spring-integration-test/src/test/java/org/springframework/integration/test/matcher/MapContainsTests.java rename to spring-integration-test-support/src/test/java/org/springframework/integration/test/matcher/MapContainsTests.java diff --git a/spring-integration-test/src/test/java/org/springframework/integration/test/matcher/MockitoMessageMatchersTests.java b/spring-integration-test-support/src/test/java/org/springframework/integration/test/matcher/MockitoMessageMatchersTests.java similarity index 100% rename from spring-integration-test/src/test/java/org/springframework/integration/test/matcher/MockitoMessageMatchersTests.java rename to spring-integration-test-support/src/test/java/org/springframework/integration/test/matcher/MockitoMessageMatchersTests.java diff --git a/spring-integration-test/src/test/java/org/springframework/integration/test/matcher/PayloadMatcherTests.java b/spring-integration-test-support/src/test/java/org/springframework/integration/test/matcher/PayloadMatcherTests.java similarity index 100% rename from spring-integration-test/src/test/java/org/springframework/integration/test/matcher/PayloadMatcherTests.java rename to spring-integration-test-support/src/test/java/org/springframework/integration/test/matcher/PayloadMatcherTests.java diff --git a/spring-integration-test/src/test/java/org/springframework/integration/test/util/SocketUtilsTests.java b/spring-integration-test-support/src/test/java/org/springframework/integration/test/util/SocketUtilsTests.java similarity index 100% rename from spring-integration-test/src/test/java/org/springframework/integration/test/util/SocketUtilsTests.java rename to spring-integration-test-support/src/test/java/org/springframework/integration/test/util/SocketUtilsTests.java diff --git a/spring-integration-test-support/src/test/resources/log4j.properties b/spring-integration-test-support/src/test/resources/log4j.properties new file mode 100644 index 0000000000..538a5c4c9e --- /dev/null +++ b/spring-integration-test-support/src/test/resources/log4j.properties @@ -0,0 +1,7 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%c{1}: %m%n + +log4j.category.org.springframework.integration=WARN 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 new file mode 100644 index 0000000000..593658be1d --- /dev/null +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/IntegrationEndpointsInitializer.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017 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 + * + * http://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.Map; + +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.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. + * + * @author Artem Bilan + * + * @since 5.0 + */ +class IntegrationEndpointsInitializer implements SmartInitializingSingleton, BeanFactoryAware { + + private final SpringIntegrationTest springIntegrationTest; + + private ConfigurableListableBeanFactory beanFactory; + + IntegrationEndpointsInitializer(SpringIntegrationTest springIntegrationTest) { + this.springIntegrationTest = springIntegrationTest; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory); + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + + @Override + public void afterSingletonsInstantiated() { + Map endpoints = this.beanFactory.getBeansOfType(AbstractEndpoint.class); + + endpoints.entrySet() + .stream() + .filter(entry -> match(entry.getKey())) + .forEach(entry -> entry.getValue().setAutoStartup(false)); + } + + private boolean match(String name) { + for (String pattern : this.springIntegrationTest.noAutoStartup()) { + if (PatternMatchUtils.simpleMatch(pattern, name)) { + return true; + } + } + return false; + } + +} 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 new file mode 100644 index 0000000000..fa13bb4cf9 --- /dev/null +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContext.java @@ -0,0 +1,129 @@ +/* + * Copyright 2017 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 + * + * http://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 java.util.Collection; +import java.util.HashMap; +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.config.ConfigurableListableBeanFactory; +import org.springframework.context.Lifecycle; +import org.springframework.integration.core.MessageSource; +import org.springframework.integration.endpoint.SourcePollingChannelAdapter; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A {@link BeanFactoryAware} component with an API to customize real beans + * in the application context from test code. + *

+ * The bean for this class is registered automatically via the {@link SpringIntegrationTest} + * annotation and can be autowired into test class. + * + * @author Artem Bilan + * + * @since 5.0 + * @see SpringIntegrationTest + */ +public class MockIntegrationContext implements BeanFactoryAware { + + public static final String MOCK_INTEGRATION_CONTEXT_BEAN_NAME = "mockIntegrationContext"; + + private final Map beans = new HashMap<>(); + + private ConfigurableListableBeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + Assert.isAssignable(ConfigurableListableBeanFactory.class, beanFactory.getClass(), + "a ConfigurableListableBeanFactory is required"); + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + + /** + * Reinstate the mocked beans after execution test to their real state. + * Typically is used from the {@link org.junit.After} method. + * @param beanNames the bean names to reset. + * If {@code null}, all the mocked beans are reset + */ + public void resetBeans(String... beanNames) { + final Collection names; + if (!ObjectUtils.isEmpty(beanNames)) { + names = Arrays.asList(beanNames); + } + else { + names = null; + } + + this.beans.entrySet() + .stream() + .filter(e -> names == null || names.contains(e.getKey())) + .forEach(e -> { + Object endpoint = this.beanFactory.getBean(e.getKey()); + DirectFieldAccessor directFieldAccessor = new DirectFieldAccessor(endpoint); + if (endpoint instanceof SourcePollingChannelAdapter) { + directFieldAccessor.setPropertyValue("source", e.getValue()); + } + }); + } + + /** + * Replace the real {@link MessageSource} in the {@link SourcePollingChannelAdapter} bean + * with provided {@link MessageSource} instance. + * Can be a mock object. + * @param pollingAdapterId the endpoint bean name + * @param mockMessageSource the {@link MessageSource} to replace in the endpoint bean + * @see org.springframework.integration.test.mock.MockIntegration#mockMessageSource + */ + public void instead(String pollingAdapterId, MessageSource mockMessageSource) { + instead(pollingAdapterId, mockMessageSource, true); + } + + /** + * Replace the real {@link MessageSource} in the {@link SourcePollingChannelAdapter} bean + * with provided {@link MessageSource} instance. + * Can be a mock object. + * The endpoint is not started when {@code autoStartup == false}. + * @param pollingAdapterId the endpoint bean name + * @param mockMessageSource the {@link MessageSource} to replace in the endpoint bean + * @param autoStartup start or not the endpoint after replacing its {@link MessageSource} + * @see org.springframework.integration.test.mock.MockIntegration#mockMessageSource + */ + public void instead(String pollingAdapterId, MessageSource mockMessageSource, boolean autoStartup) { + instead(pollingAdapterId, mockMessageSource, SourcePollingChannelAdapter.class, "source", autoStartup); + } + + private void instead(String endpointId, Object mock, Class endpointClass, String property, + boolean autoStartup) { + Object endpoint = this.beanFactory.getBean(endpointId, endpointClass); + if (autoStartup && endpoint instanceof Lifecycle) { + ((Lifecycle) endpoint).stop(); + } + DirectFieldAccessor directFieldAccessor = new DirectFieldAccessor(endpoint); + this.beans.put(endpointId, directFieldAccessor.getPropertyValue(property)); + directFieldAccessor.setPropertyValue("source", mock); + if (autoStartup && endpoint instanceof Lifecycle) { + ((Lifecycle) endpoint).start(); + } + } + +} 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 new file mode 100644 index 0000000000..5cf453bd66 --- /dev/null +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizer.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017 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 + * + * http://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.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; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.Assert; + +/** + * The {@link ContextCustomizer} implementation for Spring Integration specific environment. + *

+ * Registers {@link MockIntegrationContext}, {@link IntegrationEndpointsInitializer} beans. + * + * @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(); + Assert.isInstanceOf(BeanDefinitionRegistry.class, beanFactory); + BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; + + 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()); + + } + +} + 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 new file mode 100644 index 0000000000..21b18b5d08 --- /dev/null +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizerFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 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 + * + * http://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.List; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; + +/** + * The {@link ContextCustomizerFactory} implementation to produce a + * {@link MockIntegrationContextCustomizer} if a {@link SpringIntegrationTest} annotation + * is present on the test class. + * + * @author Artem Bilan + * + * @since 5.0 + */ +class MockIntegrationContextCustomizerFactory implements ContextCustomizerFactory { + + @Override + public ContextCustomizer createContextCustomizer(Class testClass, + List configAttributes) { + SpringIntegrationTest springIntegrationTest = + AnnotatedElementUtils.findMergedAnnotation(testClass, SpringIntegrationTest.class); + return springIntegrationTest != null + ? + new MockIntegrationContextCustomizer(springIntegrationTest) + : 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 new file mode 100644 index 0000000000..2d27c6a1ee --- /dev/null +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2017 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 + * + * http://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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 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 + * Framework: + *

    + *
  • Registers a {@link MockIntegrationContext} bean with the + * {@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 + * to customize {@link org.springframework.integration.endpoint.AbstractEndpoint} + * beans with provided options on this annotation. + *
  • + *
+ *

+ * The typical usage of this annotation is like: + *

+ * @RunWith(SpringRunner.class)
+ * @SpringIntegrationTest
+ * public class MyIntegrationTests {
+ *
+ *    @@Autowired
+ *    private MockIntegrationContext mockIntegrationContext;
+ *
+ * }
+ * 
+ * + * @author Artem Bilan + * + * @since 5.0 + * + * @see MockIntegrationContext + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface SpringIntegrationTest { + + /** + * Specify a simple matching patterns ("xxx*", "*xxx", "*xxx*" or "xxx*yyy") for + * {@link org.springframework.integration.endpoint.AbstractEndpoint} + * 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 org.springframework.util.PatternMatchUtils + */ + String[] noAutoStartup() default {}; + +} diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/context/package-info.java b/spring-integration-test/src/main/java/org/springframework/integration/test/context/package-info.java new file mode 100644 index 0000000000..22850ca6af --- /dev/null +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/package-info.java @@ -0,0 +1,4 @@ +/** + * Test context-related components. + */ +package org.springframework.integration.test.context; diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/mock/MockIntegration.java b/spring-integration-test/src/main/java/org/springframework/integration/test/mock/MockIntegration.java new file mode 100644 index 0000000000..687db51eb2 --- /dev/null +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/mock/MockIntegration.java @@ -0,0 +1,120 @@ +/* + * Copyright 2017 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 + * + * http://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.mock; + +import static org.mockito.BDDMockito.given; + +import java.util.ArrayList; +import java.util.List; + +import org.mockito.Mockito; + +import org.springframework.integration.core.MessageSource; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; + +/** + * The factory for integration specific mock components. + * + * @author Artem Bilan + * + * @since 5.0 + */ +public final class MockIntegration { + + /** + * Build a mock for the {@link MessageSource} based on the provided payload. + * The returned instance is ordinary Mockito mock that is capable of + * recording interactions with it and further verification. + * @param payload the payload to return by mocked {@link MessageSource} + * @param the payload type + * @return the mocked {@link MessageSource} + * @see Mockito#mock(Class) + */ + @SuppressWarnings("unchecked") + public static MessageSource mockMessageSource(T payload) { + return (MessageSource) mockMessageSource(new GenericMessage<>(payload)); + } + + /** + * Build a mock for the {@link MessageSource} based on the provided payloads. + * The returned instance is ordinary Mockito mock that is capable of + * recording interactions with it and further verification. + * @param payload the first payload to return by mocked {@link MessageSource} + * @param payloads the next payloads to return by mocked {@link MessageSource} + * @param the payload type + * @return the mocked {@link MessageSource} + * @see Mockito#mock(Class) + */ + @SuppressWarnings("unchecked") + public static MessageSource mockMessageSource(T payload, T... payloads) { + List> messages = null; + + if (payloads != null) { + messages = new ArrayList<>(payloads.length); + for (T p : payloads) { + messages.add(new GenericMessage<>(p)); + } + } + + return (MessageSource) mockMessageSource(new GenericMessage<>(payload), + (messages != null + ? messages.toArray(new Message[messages.size()]) + : null)); + } + + /** + * Build a mock for the {@link MessageSource} based on the provided message. + * The returned instance is ordinary Mockito mock that is capable of + * recording interactions with it and further verification. + * @param message the message to return by mocked {@link MessageSource} + * @return the mocked {@link MessageSource} + * @see Mockito#mock(Class) + */ + @SuppressWarnings("rawtypes") + public static MessageSource mockMessageSource(Message message) { + MessageSource messageSource = Mockito.mock(MessageSource.class); + + given(messageSource.receive()) + .>willReturn(message); + + return messageSource; + } + + /** + * Build a mock for the {@link MessageSource} based on the provided messages. + * The returned instance is ordinary Mockito mock that is capable of + * recording interactions with it and further verification. + * @param message the first message to return by mocked {@link MessageSource} + * @param messages the next messages to return by mocked {@link MessageSource} + * @return the mocked {@link MessageSource} + * @see Mockito#mock(Class) + */ + @SuppressWarnings("rawtypes") + public static MessageSource mockMessageSource(Message message, Message... messages) { + MessageSource messageSource = Mockito.mock(MessageSource.class); + + given(messageSource.receive()) + .willReturn(message, messages); + + return messageSource; + } + + private MockIntegration() { + } + +} diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/mock/package-info.java b/spring-integration-test/src/main/java/org/springframework/integration/test/mock/package-info.java new file mode 100644 index 0000000000..94d36e70f3 --- /dev/null +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/mock/package-info.java @@ -0,0 +1,4 @@ +/** + * Utilities for mocking integration components. + */ +package org.springframework.integration.test.mock; diff --git a/spring-integration-test/src/main/resources/META-INF/spring.factories b/spring-integration-test/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..8926d5bd23 --- /dev/null +++ b/spring-integration-test/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Spring Test ContextCustomizerFactories +org.springframework.test.context.ContextCustomizerFactory=\ +org.springframework.integration.test.context.MockIntegrationContextCustomizerFactory diff --git a/spring-integration-test/src/test/java/org/springframework/integration/test/mock/MockMessageSourceTests-context.xml b/spring-integration-test/src/test/java/org/springframework/integration/test/mock/MockMessageSourceTests-context.xml new file mode 100644 index 0000000000..43297e9def --- /dev/null +++ b/spring-integration-test/src/test/java/org/springframework/integration/test/mock/MockMessageSourceTests-context.xml @@ -0,0 +1,20 @@ + + + + + + + + + b + c + + + + + + 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 new file mode 100644 index 0000000000..98b5032a2e --- /dev/null +++ b/spring-integration-test/src/test/java/org/springframework/integration/test/mock/MockMessageSourceTests.java @@ -0,0 +1,229 @@ +/* + * Copyright 2017 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 + * + * http://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.mock; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.Lifecycle; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.integration.annotation.InboundChannelAdapter; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.core.MessageSource; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.dsl.IntegrationFlows; +import org.springframework.integration.dsl.PollerSpec; +import org.springframework.integration.dsl.Pollers; +import org.springframework.integration.dsl.StandardIntegrationFlow; +import org.springframework.integration.dsl.context.IntegrationFlowContext; +import org.springframework.integration.dsl.context.IntegrationFlowRegistration; +import org.springframework.integration.scheduling.PollerMetadata; +import org.springframework.integration.test.context.MockIntegrationContext; +import org.springframework.integration.test.context.SpringIntegrationTest; +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; + +/** + * @author Artem Bilan + * + * @since 5.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = MockMessageSourceTests.Config.class) +@SpringIntegrationTest(noAutoStartup = {"inboundChannelAdapter", "*Source*"}) +@DirtiesContext +public class MockMessageSourceTests { + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private MockIntegrationContext mockIntegrationContext; + + @Autowired + private QueueChannel results; + + @Autowired + private IntegrationFlowContext integrationFlowContext; + + @After + public void tearDown() { + this.mockIntegrationContext.resetBeans("mySourceEndpoint", "inboundChannelAdapter"); + results.purge(null); + } + + @Test + public void testMockMessageSource() { + this.mockIntegrationContext.instead("mySourceEndpoint", + MockIntegration.mockMessageSource("foo", "bar", "baz")); + + Message receive = this.results.receive(10_000); + assertNotNull(receive); + assertEquals("FOO", receive.getPayload()); + + receive = this.results.receive(10_000); + assertNotNull(receive); + assertEquals("BAR", receive.getPayload()); + + for (int i = 0; i < 10; i++) { + receive = this.results.receive(10_000); + assertNotNull(receive); + assertEquals("BAZ", receive.getPayload()); + } + + this.applicationContext.getBean("mySourceEndpoint", Lifecycle.class).stop(); + } + + @Test + public void testMockMessageSourceInConfig() { + this.applicationContext.getBean("mockMessageSourceTests.Config.testingMessageSource.inboundChannelAdapter", + Lifecycle.class).start(); + + Message receive = this.results.receive(10_000); + assertNotNull(receive); + assertEquals(1, receive.getPayload()); + + receive = this.results.receive(10_000); + assertNotNull(receive); + assertEquals(2, receive.getPayload()); + + for (int i = 0; i < 10; i++) { + receive = this.results.receive(10_000); + assertNotNull(receive); + assertEquals(3, receive.getPayload()); + } + + this.applicationContext.getBean("mockMessageSourceTests.Config.testingMessageSource.inboundChannelAdapter", + Lifecycle.class).stop(); + } + + @Test + public void testMockMessageSourceInXml() { + this.applicationContext.getBean("inboundChannelAdapter", Lifecycle.class).start(); + + Message receive = this.results.receive(10_000); + assertNotNull(receive); + assertEquals("a", receive.getPayload()); + + receive = this.results.receive(10_000); + assertNotNull(receive); + assertEquals("b", receive.getPayload()); + + for (int i = 0; i < 10; i++) { + receive = this.results.receive(10_000); + assertNotNull(receive); + assertEquals("c", receive.getPayload()); + } + + this.applicationContext.getBean("inboundChannelAdapter", Lifecycle.class).stop(); + } + + @Test + public void testMockMessageSourceDynamicFlow() { + QueueChannel out = new QueueChannel(); + StandardIntegrationFlow flow = IntegrationFlows + .from(MockIntegration.mockMessageSource("foo", "bar", "baz")) + .transform(String::toUpperCase) + .channel(out) + .get(); + IntegrationFlowRegistration registration = this.integrationFlowContext.registration(flow).register(); + + Message receive = out.receive(10_000); + assertNotNull(receive); + assertEquals("FOO", receive.getPayload()); + + receive = out.receive(10_000); + assertNotNull(receive); + assertEquals("BAR", receive.getPayload()); + + for (int i = 0; i < 10; i++) { + receive = out.receive(10_000); + assertNotNull(receive); + assertEquals("BAZ", receive.getPayload()); + } + + registration.destroy(); + } + + @Test + public void testWrongBeanForInstead() { + try { + this.mockIntegrationContext.instead("errorChannel", () -> null); + fail("BeanNotOfRequiredTypeException expected"); + } + catch (Exception e) { + assertThat(e, instanceOf(BeanNotOfRequiredTypeException.class)); + assertThat(e.getMessage(), + containsString("Bean named 'errorChannel' is expected to be of type " + + "'org.springframework.integration.endpoint.SourcePollingChannelAdapter' " + + "but was actually of type " + + "'org.springframework.integration.channel.PublishSubscribeChannel")); + + } + } + + @Configuration + @EnableIntegration + @ImportResource("org/springframework/integration/test/mock/MockMessageSourceTests-context.xml") + public static class Config { + + @Bean(name = PollerMetadata.DEFAULT_POLLER) + public PollerSpec defaultPoller() { + return Pollers.fixedDelay(10); + } + + @Bean + public IntegrationFlow myFlow() { + return IntegrationFlows + .from(() -> new GenericMessage<>("myData"), + e -> e.id("mySourceEndpoint")) + .transform(String::toUpperCase) + .channel(results()) + .get(); + } + + @Bean + public QueueChannel results() { + return new QueueChannel(); + } + + @InboundChannelAdapter(channel = "results") + @Bean + public MessageSource testingMessageSource() { + return MockIntegration.mockMessageSource(1, 2, 3); + } + + } + +} diff --git a/spring-integration-test/src/test/resources/log4j.properties b/spring-integration-test/src/test/resources/log4j.properties index 538a5c4c9e..fbc5e40664 100644 --- a/spring-integration-test/src/test/resources/log4j.properties +++ b/spring-integration-test/src/test/resources/log4j.properties @@ -2,6 +2,7 @@ log4j.rootCategory=WARN, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%c{1}: %m%n +log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2}:%L - %m%n +log4j.category.org.springframework.integration.test=INFO log4j.category.org.springframework.integration=WARN diff --git a/src/reference/asciidoc/index.adoc b/src/reference/asciidoc/index.adoc index 05b5ed55b7..3ded095582 100644 --- a/src/reference/asciidoc/index.adoc +++ b/src/reference/asciidoc/index.adoc @@ -125,10 +125,12 @@ include::./transactions.adoc[] [appendix] include::./security.adoc[] [appendix] -include::./samples.adoc[] -[appendix] include::./configuration.adoc[] [appendix] +include::./testing.adoc[] +[appendix] +include::./samples.adoc[] +[appendix] include::./resources.adoc[] [appendix] include::./history.adoc[] diff --git a/src/reference/asciidoc/testing.adoc b/src/reference/asciidoc/testing.adoc new file mode 100644 index 0000000000..4727898beb --- /dev/null +++ b/src/reference/asciidoc/testing.adoc @@ -0,0 +1,294 @@ +[[testing]] +== Testing support + +Spring Integration provides a number of utilities and annotations to help when testing your application. +Test support is presented by two modules: `spring-integration-test-support` which contains core items and shared utilities, and `spring-integration-test` which provides mocking and application context configuration components for integration tests. + +`spring-integration-test-support` (`spring-integration-test` in versions before _5.0_) provides basic, standalone utilities, rules and matchers for unit testing (it also has no dependencies on Spring Integration itself, and is used internally in Framework tests). `spring-integration-test` is aimed to help with integration testing and provides a comprehensive high level API to mock integration components and verify behavior of individual components, including whole integration flows or just parts thereof. +A thorough treatment of testing in the enterprise is beyond the scope of this reference manual. +See the http://www.enterpriseintegrationpatterns.com/docs/TestDrivenEAI.pdf[Test-Driven Development in Enterprise Integration Projects] paper, by Gregor Hohpe and Wendy Istvanick, for a source of ideas and principles for testing your target integration solution. + +[[testing-intro]] +=== Introduction + +The Spring Integration Test Framework and test utilities are fully based on existing JUnit, Hamcrest and Mockito libraries. +The Application Context interaction is based on the http://docs.spring.io/spring-framework/docs/current/spring-framework-reference/htmlsingle/#testing[Spring Test Framework]. +Please, refer to the documentation for those projects for further information. + +Thanks to the canonical implementation of the EIP in Spring Integration Framework and its first class citizens like `MessageChannel`, `Endpoint` and `MessageHandler` abstractions and supported out-of-the-box loose coupling principles, we can implement integration solutions of any complexity. +With the Spring Integration API for the flow definitions, we can improve, modify or even replace some part of the flow without impacting (mostly) other components in the integration solution. +Testing such an integration solution is still a challenge, from an __end-to-end__ perspective, as well as with an __in-isolation__ approach. +There are several existing tools which help to test or mock some integration protocols and they work very well with Spring Integration Channel Adapters; examples include: + +- Spring `MockMVC` and its `MockRestServiceServer` for HTTP; +- Some RDBMS vendors provide embedded data bases for JDBC or JPA support; +- ActiveMQ can be embedded for testing JMS or STOMP protocols; +- There are tools for embedded MongoDB and Redis; +- Tomcat and Jetty have embedded libraries to test real HTTP, Web Services or WebSockets; +- The `FtpServer` and `SshServer` from the Apache Mina project can be used for testing (S)FTP protocols; +- Gemfire and Hazelcast can be run as real data grid nodes in the tests; +- The Curator Framework provides a `TestingServer` for Zookeeper interaction; +- Apache Kafka provides admin tools to embed a Kafka Broker in the tests. + +Most of these tools and libraries are used in Spring Integration tests and from the GitHub https://github.com/spring-projects/spring-integration[repository], in the `test` directory of each module, you can discover ideas how to build your own tests for integration solutions. + +The rest of this chapter describes the testing tools and utilities provided by the Spring Integration Framework. + +[[testing-utilities]] +=== Testing Utilities + +The `spring-integration-test-support` module provides utilities and helpers for unit testing. + +==== TestUtils + +The `TestUtils` class is mostly used for properties assertions in JUnit tests: +[source,java] +---- +@Test +public void loadBalancerRef() { + MessageChannel channel = channels.get("lbRefChannel"); + LoadBalancingStrategy lbStrategy = TestUtils.getPropertyValue(channel, + "dispatcher.loadBalancingStrategy", LoadBalancingStrategy.class); + assertTrue(lbStrategy instanceof SampleLoadBalancingStrategy); +} +---- + +`TestUtils.getPropertyValue()` is based on Spring's `DirectFieldAccessor` and provides the ability to get a value from the target private property. +As you see by the example above it also supports nested properties access, using dotted notation. + +The `createTestApplicationContext()` factory method produce a `TestApplicationContext` instance with the supplied Spring Integration environment. + +See the JavaDocs of other `TestUtils` methods for more information about this class. + +==== SocketUtils + +The `SocketUtils` provides several methods to select a random port(s) for exposing server-side components without conflicts: + +[source,xml] +---- + + + + + + + +---- + +Which is used from the unit test as: + +[source,java] +---- +@Autowired @Qualifier("syslog.adapter") +private UdpSyslogReceivingChannelAdapter adapter; + +@Autowired +private PollableChannel sysLogs; +... +@Test +public void testSimplestUdp() throws Exception { + int port = TestUtils.getPropertyValue(adapter1, "udpAdapter.port", Integer.class); + byte[] buf = "<157>JUL 26 22:08:35 WEBERN TESTING[70729]: TEST SYSLOG MESSAGE".getBytes("UTF-8"); + DatagramPacket packet = new DatagramPacket(buf, buf.length, + new InetSocketAddress("localhost", port)); + DatagramSocket socket = new DatagramSocket(); + socket.send(packet); + socket.close(); + Message message = foo.receive(10000); + assertNotNull(message); +} +---- + +NOTE: This tecnique is not foolproof; some other process could be allocated the "free" port before your test opens it. +It is generally more preferable to use a server port `0` and let the operating system select the port for you, then discover the selected port in your test. +We have converted most framework tests to use this preferred technique. + +==== OnlyOnceTrigger + +The `OnlyOnceTrigger` is useful for polling endpoints when it is good to produce only one test message and verify the behavior without impacting of unexpected other period messages: + +[source,xml] +---- + + + + + +---- + +[source,java] +---- +@Autowired +@Qualifier("jpaPoller") +PollerMetadata poller; + +@Autowired +OnlyOnceTrigger testTrigger; +... +@Test +@DirtiesContext +public void testWithEntityClass() throws Exception { + this.testTrigger.reset(); + ... + JpaPollingChannelAdapter jpaPollingChannelAdapter = new JpaPollingChannelAdapter(jpaExecutor); + + SourcePollingChannelAdapter adapter = JpaTestUtils.getSourcePollingChannelAdapter( + jpaPollingChannelAdapter, this.outputChannel, this.poller, this.context, + this.getClass().getClassLoader()); + adapter.start(); + ... +} +---- + +==== Support Components + +The `org.springframework.integration.test.support` package contains various abstract classes which should be implemented in target tests. +See their JavaDocs for more information. + +==== Hamcrest and Mockito Matchers + +The `org.springframework.integration.test.matcher` package contains several `Matcher` implementations to assert `Message` and its properties in unit tests: +[source,java] +---- +import static org.springframework.integration.test.matcher.PayloadMatcher.hasPayload; +... +@Test +public void transform_withFilePayload_convertedToByteArray() throws Exception { + Message result = this.transformer.transform(message); + assertThat(result, is(notNullValue())); + assertThat(result, hasPayload(is(instanceOf(byte[].class)))); + assertThat(result, hasPayload(SAMPLE_CONTENT.getBytes(DEFAULT_ENCODING))); +} +---- + +The `MockitoMessageMatchers` factory can be used for mocks stubbing and verifications: + +[source,java] +---- +static final Date SOME_PAYLOAD = new Date(); + +static final String SOME_HEADER_VALUE = "bar"; + +static final String SOME_HEADER_KEY = "test.foo"; +... +Message message = MessageBuilder.withPayload(SOME_PAYLOAD) + .setHeader(SOME_HEADER_KEY, SOME_HEADER_VALUE) + .build(); +MessageHandler handler = mock(MessageHandler.class); +handler.handleMessage(message); +verify(handler).handleMessage(messageWithPayload(SOME_PAYLOAD)); +verify(handler).handleMessage(messageWithPayload(is(instanceOf(Date.class)))); +... +MessageChannel channel = mock(MessageChannel.class); +when(channel.send(messageWithHeaderEntry(SOME_HEADER_KEY, is(instanceOf(Short.class))))) + .thenReturn(true); +assertThat(channel.send(message), is(false)); +---- + +Additional utilities will eventually be added or migrated. +For example `RemoteFileTestSupport` implementations for the (S)FTP tests can be moved from the `test` directory of those particular modules to this `spring-integration-test-support` artifact. + +[[test-context]] +=== Spring Integration and test context + +Typically, tests for Spring applications use the Spring Test Framework and since Spring Integration is based on the Spring Framework foundation, everything we can do with the Spring Test Framework is applied as well when testing integration flows. +The `org.springframework.integration.test.context` package provides some components for enhancing the test context for integration needs. +First of all we configure our test class with a `@SpringIntegrationTest` annotation to enable the Spring Integration Test Framework: + +[source,java] +---- +@RunWith(SpringRunner.class) +@SpringIntegrationTest(noAutoStartup = {"inboundChannelAdapter", "*Source*"}) +public class MyIntegrationTests { + + @Autowired + private MockIntegrationContext mockIntegrationContext; + +} +---- + +The `@SpringIntegrationTest` annotation populates a `MockIntegrationContext` bean which can be autowired to the test class to access its methods. +With the provided `noAutoStartup` option, the Spring Integration Test Framework prevents endpoints that are normally `autoStartup=true` from starting. The endpoints are matched to the provided patterns, which support the following simple pattern styles: `xxx*`, `*xxx`, `*xxx*` and `xxx*yyy`. + +This is useful, when we would like to not have real connections to the target systems from Inbound Channel Adapters, for example an AMQP Inbound Gateway, JDBC Polling Channel Adapter, WebSocket Message Producer in client mode etc. + +The `MockIntegrationContext` is aimed to be used in the target test-cases for modifications to beans in the real application context, for example those endpoints that have `autoStartup` overridden to false can be replaced with mocks: + +[source,java] +---- +@Test +public void testMockMessageSource() { + MessageSource messageSource = () -> new GenericMessage<>("foo"); + + this.mockIntegrationContext.instead("mySourceEndpoint", messageSource); + + Message receive = this.results.receive(10_000); + assertNotNull(receive); +} +---- + +See their JavaDocs for more information. + +[[testing-mocks]] +=== Integration Mocks + +The `org.springframework.integration.test.mock` package offers tools and utilities for mocking, stubbing and verification of activity on Spring Integration components. +The mocking functionality is fully based and compatible with the well known Mockito Framework. +(The current Mockito transitive dependency is of _version 2.5.x_.) + +==== MockIntegration + +The `MockIntegration` factory provides an API to build mocks for Spring Integration beans which are parts of the integration flow - `MessageSource`, `MessageProducer`, `MessageHandler`, `MessageChannel`. +The target mocks can be used during configuration phase: + +[source,xml] +---- + + + + + + b + c + + + + +---- + +[source,java] +---- +@InboundChannelAdapter(channel = "results") +@Bean +public MessageSource testingMessageSource() { + return MockIntegration.mockMessageSource(1, 2, 3); +} +... +StandardIntegrationFlow flow = IntegrationFlows + .from(MockIntegration.mockMessageSource("foo", "bar", "baz")) + .transform(String::toUpperCase) + .channel(out) + .get(); +IntegrationFlowRegistration registration = this.integrationFlowContext.registration(flow) + .register(); +---- + +as well as in the target test method to replace the real endpoints before performing verifications and assertions. +For this purpose, the aforementioned `MockIntegrationContext` should be used from the test: + +[source,java] +---- +this.mockIntegrationContext.instead("mySourceEndpoint", + MockIntegration.mockMessageSource("foo", "bar", "baz")); +Message receive = this.results.receive(10_000); +assertNotNull(receive); +assertEquals("FOO", receive.getPayload()); +---- + +[[testing-other-resources]] +=== Other Resources + +As well as exploring the test cases in the framework itself, the https://github.com/spring-projects/spring-integration-samples[spring-integration-samples repository] has some sample apps specifically around testing, such as `testing-examples` and `advanced-testing-examples`. +In some cases, the samples themselves have comprehensive end-to-end tests, such as the `file-split-ftp` sample. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 194976f85e..629bd441e4 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -9,6 +9,19 @@ development process. [[x5.0-new-components]] === New Components +==== Java DSL + +The separate https://github.com/spring-projects/spring-integration-java-dsl[Spring Integration Java DSL] project has now been merged into the core Spring Integration project. +The `IntegrationComponentSpec` implementations for channel adapters and gateways are distributed to their specific modules. +See <> for more information about Java DSL support for target protocol integration. +Also see the https://github.com/spring-projects/spring-integration/wiki/Spring-Integration-4.3-to-5.0-Migration-Guide#java-dsl[4.3 to 5.0 Migration Guide] for the required steps to move to Spring Integration 5.0. + +==== Testing Support + +A new Spring Integration Test Framework has been created to assist with testing Spring Integration applications. +Now, with the `@SpringIntegrationTest` annotation on test class and `MockIntegration` factory you can make your JUnit tests for integration flows somewhat easier. +See <> for more information. + ==== MongoDB Outbound Gateway The new `MongoDbOutboundGateway` allows you to make queries to the database on demand by sending a message to its request channel. diff --git a/src/reference/asciidoc/xml.adoc b/src/reference/asciidoc/xml.adoc index e71b7d366b..7c0759b845 100644 --- a/src/reference/asciidoc/xml.adoc +++ b/src/reference/asciidoc/xml.adoc @@ -75,7 +75,7 @@ Here is an overview of all available configuration parameters of the `xpath-expr ---- -<1> Defines an XPath xpression. +<1> Defines an XPath expression. _Required_. @@ -89,12 +89,12 @@ It is not valid to specify both this attribute and the `map` sub element, or set _Optional_. -<4> Allows you to set the namspace prefix directly as and attribute on the XPath expression element. +<4> Allows you to set the namespace prefix directly as and attribute on the XPath expression element. If you set `ns-prefix`, you must also set the `ns-uri` attribute. _Optional_. -<5> Allows you to set the namspace URI directly as an attribute on the XPath expression element. +<5> Allows you to set the namespace URI directly as an attribute on the XPath expression element. If you set `ns-uri`, you must also set the `ns-prefix` attribute. _Optional_.