INT-4251: Add Initial Testing Framwork and Docs

JIRA: https://jira.spring.io/browse/INT-4251

MockIntegration PoC

Demonstrate how `MockIntegration.mockMessageSource()` can be used.
Even from the XML Config!
The same semantics can be applied for any other our mocking features we will add to the `MockIntegration`

Add `testMockMessageSourceDynamicFlow()`

Improve `MockIntegration.Context.instead()` for target bean type assertion

Some refactoring, improvements and JavaDocs

* Move `MockIntegration.Context` to its own `MockIntegrationContext` class
* Rename `@MockIntegrationTest` to `@SpringIntegrationTest` since we talk there not only about mocks, but any other possible testing feature for integration
* Add `@SpringIntegrationTest#stopEndpoints()` attribute for endpoints bean names patterns
* Add `IntegrationEndpointsInitializer` to customize `AbstractEndpoint`s according options from the `@SpringIntegrationTest`
Right now it is only about making them `autoStartup = false`

* Rename `SI-test` to `SI-test-support`
* Rename `SI-mock` to `SI-test`
* Rename `@SpringIntegrationTest#stopEndpoints()` to `noAutoStartup()`
* Fix JavaDocs according PR comments
* Increase timeouts in some sporadically failed tests
* Draft for the `testing.adoc`

Fixes spring-projects/spring-integration-java-dsl#23

Fix `testing.adoc` a bit

* Document Testing Support
* Fix a couple typos in the `xml.adoc`
* Add `package-info.java` files to `/test/` sub-packages

Improve docs according PR comments

Fix typos in `TestUtils` JavaDocs

Add `What's New` note about Java DSL and Testing support

Doc Polishing
This commit is contained in:
Artem Bilan
2017-02-13 17:46:14 -05:00
committed by Gary Russell
parent 610ad7e0e1
commit 2862c449c5
52 changed files with 1173 additions and 33 deletions

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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<String>("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));

View File

@@ -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<String>("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());

View File

@@ -0,0 +1,4 @@
/**
* Provides testing utils for mail support.
*/
package org.springframework.integration.test.mail;

View File

@@ -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 <T> 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> T getPropertyValue(Object root, String propertyPath, Class<T> 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<Class<?>> interfaces = ClassUtils.getAllInterfacesAsSet(channel);
final AtomicReference<String> componentName = new AtomicReference<String>();
@@ -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);
}
}
/**

View File

@@ -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

View File

@@ -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<String, AbstractEndpoint> 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;
}
}

View File

@@ -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.
* <p>
* 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<String, Object> 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<String> 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();
}
}
}

View File

@@ -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.
* <p>
* 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());
}
}

View File

@@ -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<ContextConfigurationAttributes> configAttributes) {
SpringIntegrationTest springIntegrationTest =
AnnotatedElementUtils.findMergedAnnotation(testClass, SpringIntegrationTest.class);
return springIntegrationTest != null
?
new MockIntegrationContextCustomizer(springIntegrationTest)
: null;
}
}

View File

@@ -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 <em>Spring TestContext
* Framework</em>:
* <ul>
* <li>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.
* </li>
* <li>Registers an {@link IntegrationEndpointsInitializer} bean which is used
* to customize {@link org.springframework.integration.endpoint.AbstractEndpoint}
* beans with provided options on this annotation.
* </li>
* </ul>
* <p>
* The typical usage of this annotation is like:
* <pre class="code">
* &#064;RunWith(SpringRunner.class)
* &#064;SpringIntegrationTest
* public class MyIntegrationTests {
*
* &#064;@Autowired
* private MockIntegrationContext mockIntegrationContext;
*
* }
* </pre>
*
* @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 {};
}

View File

@@ -0,0 +1,4 @@
/**
* Test context-related components.
*/
package org.springframework.integration.test.context;

View File

@@ -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 <T> the payload type
* @return the mocked {@link MessageSource}
* @see Mockito#mock(Class)
*/
@SuppressWarnings("unchecked")
public static <T> MessageSource<T> mockMessageSource(T payload) {
return (MessageSource<T>) 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 <T> the payload type
* @return the mocked {@link MessageSource}
* @see Mockito#mock(Class)
*/
@SuppressWarnings("unchecked")
public static <T> MessageSource<T> mockMessageSource(T payload, T... payloads) {
List<Message<T>> messages = null;
if (payloads != null) {
messages = new ArrayList<>(payloads.length);
for (T p : payloads) {
messages.add(new GenericMessage<>(p));
}
}
return (MessageSource<T>) 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())
.<Message<?>>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() {
}
}

View File

@@ -0,0 +1,4 @@
/**
* Utilities for mocking integration components.
*/
package org.springframework.integration.test.mock;

View File

@@ -0,0 +1,3 @@
# Spring Test ContextCustomizerFactories
org.springframework.test.context.ContextCustomizerFactory=\
org.springframework.integration.test.context.MockIntegrationContextCustomizerFactory

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
<int:inbound-channel-adapter id="inboundChannelAdapter" channel="results">
<bean class="org.springframework.integration.test.mock.MockIntegration" factory-method="mockMessageSource">
<constructor-arg value="a"/>
<constructor-arg>
<array>
<value>b</value>
<value>c</value>
</array>
</constructor-arg>
</bean>
</int:inbound-channel-adapter>
</beans>

View File

@@ -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"))
.<String, String>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"))
.<String, String>transform(String::toUpperCase)
.channel(results())
.get();
}
@Bean
public QueueChannel results() {
return new QueueChannel();
}
@InboundChannelAdapter(channel = "results")
@Bean
public MessageSource<Integer> testingMessageSource() {
return MockIntegration.mockMessageSource(1, 2, 3);
}
}
}

View File

@@ -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

View File

@@ -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[]

View File

@@ -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]
----
<bean id="socketUtils" class="org.springframework.integration.test.util.SocketUtils" />
<int-syslog:inbound-channel-adapter id="syslog"
channel="sysLogs"
port="#{socketUtils.findAvailableUdpSocket(1514)}" />
<int:channel id="sysLogs">
<int:queue/>
</int:channel>
----
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]
----
<bean id="testTrigger" class="org.springframework.integration.test.util.OnlyOnceTrigger" />
<int:poller id="jpaPoller" trigger="testTrigger">
<int:transactional transaction-manager="transactionManager" />
</int:poller>
----
[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<String> 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]
----
<int:inbound-channel-adapter id="inboundChannelAdapter" channel="results">
<bean class="org.springframework.integration.test.mock.MockIntegration" factory-method="mockMessageSource">
<constructor-arg value="a"/>
<constructor-arg>
<array>
<value>b</value>
<value>c</value>
</array>
</constructor-arg>
</bean>
</int:inbound-channel-adapter>
----
[source,java]
----
@InboundChannelAdapter(channel = "results")
@Bean
public MessageSource<Integer> testingMessageSource() {
return MockIntegration.mockMessageSource(1, 2, 3);
}
...
StandardIntegrationFlow flow = IntegrationFlows
.from(MockIntegration.mockMessageSource("foo", "bar", "baz"))
.<String, String>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.

View File

@@ -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 <<spring-integration-endpoints>> 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 <<testing>> 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.

View File

@@ -75,7 +75,7 @@ Here is an overview of all available configuration parameters of the `xpath-expr
</int-xml:xpath-expression>
----
<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_.