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 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.
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 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:
MockMVC
and its MockRestServiceServer
for HTTP;
FtpServer
and SshServer
from the Apache Mina project can be used for testing (S)FTP protocols;
TestingServer
for Zookeeper interaction;
Most of these tools and libraries are used in Spring Integration tests and from the GitHub 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.
The spring-integration-test-support
module provides utilities and helpers for unit testing.
The TestUtils
class is mostly used for properties assertions in JUnit tests:
@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.
The SocketUtils
provides several methods to select a random port(s) for exposing server-side components without conflicts:
<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:
@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 |
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:
<bean id="testTrigger" class="org.springframework.integration.test.util.OnlyOnceTrigger" /> <int:poller id="jpaPoller" trigger="testTrigger"> <int:transactional transaction-manager="transactionManager" /> </int:poller>
@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(); ... }
The org.springframework.integration.test.support
package contains various abstract classes which should be implemented in target tests.
See their JavaDocs for more information.
The org.springframework.integration.test.matcher
package contains several Matcher
implementations to assert Message
and its properties in unit tests:
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:
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.
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:
@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:
@Test public void testMockMessageSource() { MessageSource<String> messageSource = () -> new GenericMessage<>("foo"); this.mockIntegrationContext.substituteMessageSourceFor("mySourceEndpoint", messageSource); Message<?> receive = this.results.receive(10_000); assertNotNull(receive); }
See their JavaDocs for more information.
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.)
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:
<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>
@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:
this.mockIntegrationContext.substituteMessageSourceFor("mySourceEndpoint", MockIntegration.mockMessageSource("foo", "bar", "baz")); Message<?> receive = this.results.receive(10_000); assertNotNull(receive); assertEquals("FOO", receive.getPayload());
Unlike the Mockito MessageSource
mock object, the MockMessageHandler
is just a regular AbstractMessageProducingHandler
extension with a chain API to stub handling for incoming messages.
The MockMessageHandler
provides handleNext(Consumer<Message<?>>)
to specify a one-way stub for the next request message; used to mock message handlers that don’t produce replies.
The handleNextAndReply(Function<Message<?>, ?>)
is provided for performing the same stub logic for the next request message and producing a reply for it.
They can be chained to simulate any arbitrary request-reply scenarios for all expected request messages variants.
These consumers and functions are applied to the incoming messages, one at a time from the stack, until the last, which is then used for all remaining messages.
The behavior is similar to the Mockito Answer
or doReturn()
API.
In addition, a Mockito ArgumentCaptor<Message<?>>
can be supplied to the MockMessageHandler
in a constructor argument.
Each request message for the MockMessageHandler
is captured by that ArgumentCaptor
.
During the test, its getValue()/getAllValues()
can be used to verify and assert those request messages.
The MockIntegrationContext
provides an substituteMessageHandlerFor()
API for replacing the actual configured MessageHandler
with a MockMessageHandler
, in the particular endpoint in the application context under test.
A typical usage might be:
ArgumentCaptor<Message<?>> messageArgumentCaptor = ArgumentCaptor.forClass(Message.class); MessageHandler mockMessageHandler = mockMessageHandler(messageArgumentCaptor) .handleNextAndReply(m -> m.getPayload().toString().toUpperCase()); this.mockIntegrationContext.substituteMessageHandlerFor("myService.serviceActivator", mockMessageHandler); GenericMessage<String> message = new GenericMessage<>("foo"); this.myChannel.send(message); Message<?> received = this.results.receive(10000); assertNotNull(received); assertEquals("FOO", received.getPayload()); assertSame(message, messageArgumentCaptor.getValue());
See MockIntegration
and MockMessageHandler
JavaDocs for more information.
As well as exploring the test cases in the framework itself, the 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.