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:
committed by
Gary Russell
parent
610ad7e0e1
commit
2862c449c5
14
build.gradle
14
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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides testing utils for mail support.
|
||||
*/
|
||||
package org.springframework.integration.test.mail;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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">
|
||||
* @RunWith(SpringRunner.class)
|
||||
* @SpringIntegrationTest
|
||||
* public class MyIntegrationTests {
|
||||
*
|
||||
* @@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 {};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Test context-related components.
|
||||
*/
|
||||
package org.springframework.integration.test.context;
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Utilities for mocking integration components.
|
||||
*/
|
||||
package org.springframework.integration.test.mock;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Spring Test ContextCustomizerFactories
|
||||
org.springframework.test.context.ContextCustomizerFactory=\
|
||||
org.springframework.integration.test.context.MockIntegrationContextCustomizerFactory
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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[]
|
||||
|
||||
294
src/reference/asciidoc/testing.adoc
Normal file
294
src/reference/asciidoc/testing.adoc
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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_.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user