Testing support

Spring Integration provides a number of utilities and annotations to help you test your application. Testing support is presented by two modules:

  • spring-integration-test-support contains core items and shared utilities

  • spring-integration-test 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 aims to help with integration testing and provides a comprehensive high-level API to mock integration components and verify the behavior of individual components, including whole integration flows or only parts of them.

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. See 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 (such as MessageChannel, Endpoint and MessageHandler), abstractions, and loose coupling principles, you can implement integration solutions of any complexity. With the Spring Integration API for the flow definitions, you 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, both from an end-to-end approach and from an in-isolation approach. Several existing tools can help to test or mock some integration protocols, and they work well with Spring Integration channel adapters. Examples of such tools include the following:

  • Spring MockMVC and its MockRestServiceServer can be used for testing 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 the FTP and SFTP protocols.

  • 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.

  • The GreenMail is an open source, intuitive and easy-to-use test suite of email servers for testing purposes.

Most of these tools and libraries are used in Spring Integration tests. Also, from the GitHub repository (in the test directory of each module), you can discover ideas for how to build your own tests for integration solutions.

The rest of this chapter describes the testing tools and utilities provided by Spring Integration.

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, as the following example shows:

@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 shown in the preceding example, it also supports nested properties access by using dotted notation.

The createTestApplicationContext() factory method produces a TestApplicationContext instance with the supplied Spring Integration environment.

See the Javadoc of other TestUtils methods for more information about this class.

Using OnlyOnceTrigger

OnlyOnceTrigger is useful for polling endpoints when you need to produce only one test message and verify the behavior without impacting other period messages. The following example shows how to configure OnlyOnceTrigger:

<bean id="testTrigger" class="org.springframework.integration.test.util.OnlyOnceTrigger" />

<int:poller id="jpaPoller" trigger="testTrigger">
    <int:transactional transaction-manager="transactionManager" />
</int:poller>

The following example shows how to use the preceding configuration of OnlyOnceTrigger for testing:

@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 that you should implement in target tests

JUnit Rules and Conditions

The LongRunningIntegrationTest JUnit 4 test rule is present to indicate if test should be run if RUN_LONG_INTEGRATION_TESTS environment or system property is set to true. Otherwise, it is skipped. For the same reason since version 5.1, a @LongRunningTest conditional annotation is provided for JUnit 5 tests.

Hamcrest and Mockito Matchers

The org.springframework.integration.test.matcher package contains several Matcher implementations to assert Message and its properties in unit tests. The following example shows how to use one such matcher (PayloadMatcher):

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 for stubbing and verifications, as the following example shows:

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

AssertJ conditions and predicates

Starting with version 5.2, the MessagePredicate is introduced to be used in the AssertJ matches() assertion. It requires a Message object as an expectation. And also ot can be configured with headers to exclude from expectation as well as from actual message to assert.

Spring Integration and the Test Context

Typically, tests for Spring applications use the Spring Test Framework. Since Spring Integration is based on the Spring Framework foundation, everything we can do with the Spring Test Framework also applies when testing integration flows. The org.springframework.integration.test.context package provides some components for enhancing the test context for integration needs. First we configure our test class with a @SpringIntegrationTest annotation to enable the Spring Integration Test Framework, as the following example shows:

@SpringJUnitConfig
@SpringIntegrationTest(noAutoStartup = {"inboundChannelAdapter", "*Source*"})
public class MyIntegrationTests {

    @Autowired
    private MockIntegrationContext mockIntegrationContext;

}

The @SpringIntegrationTest annotation populates a MockIntegrationContext bean, which you can autowire to the test class to access its methods. With the 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, and so on).

The @SpringIntegrationTest honors the org.springframework.test.context.NestedTestConfiguration semantics, hence it can be declared on the outer class (or even its super class) - and @SpringIntegrationTest environment will be available to inherited @Nested tests.

The MockIntegrationContext is meant to be used in the target test cases for modifications to beans in the real application context. For example, endpoints that have autoStartup overridden to false can be replaced with mocks, as the following example shows:

@Test
public void testMockMessageSource() {
    MessageSource<String> messageSource = () -> new GenericMessage<>("foo");

    this.mockIntegrationContext.substituteMessageSourceFor("mySourceEndpoint", messageSource);

    Message<?> receive = this.results.receive(10_000);
    assertNotNull(receive);
}
The mySourceEndpoint refers here to the bean name of the SourcePollingChannelAdapter for which we replace the real MessageSource with our mock. Similarly, the MockIntegrationContext.substituteMessageHandlerFor() expects a bean name for the IntegrationConsumer, which wraps a MessageHandler as an endpoint.

After test is performed you can restore the state of endpoint beans to the real configuration using MockIntegrationContext.resetBeans():

@After
public void tearDown() {
    this.mockIntegrationContext.resetBeans();
}

Starting with version 6.3, the MockIntegrationContext.substituteTriggerFor() API has been introduced. This can be used to replace the real Trigger in the AbstractPollingEndpoint. For example the production configuration may rely on daily (or even weekly) cron schedule. Any custom Trigger can be injected into the target endpoint to mitigate the time span. For example, the mentioned above OnlyOnceTrigger suggests a behavior to schedule polling task immediately and do that only once.

See the Javadoc for more information.

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 on and compatible with the well known Mockito Framework. (The current Mockito transitive dependency is on version 2.5.x or higher.)

MockIntegration

The MockIntegration factory provides an API to build mocks for Spring Integration beans that are parts of the integration flow (MessageSource, MessageProducer, MessageHandler, and MessageChannel). You can use the target mocks during the configuration phase as well as in the target test method to replace the real endpoints before performing verifications and assertions, as the following example shows:

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

The following example shows how to use Java Configuration to achieve the same configuration as the preceding example:

@InboundChannelAdapter(channel = "results")
@Bean
public MessageSource<Integer> testingMessageSource() {
    return MockIntegration.mockMessageSource(1, 2, 3);
}
...
StandardIntegrationFlow flow = IntegrationFlow
        .from(MockIntegration.mockMessageSource("foo", "bar", "baz"))
        .<String, String>transform(String::toUpperCase)
        .channel(out)
        .get();
IntegrationFlowRegistration registration = this.integrationFlowContext.registration(flow)
        .register();

For this purpose, the aforementioned MockIntegrationContext should be used from the test, as the following example shows:

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 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. It is used to mock message handlers that do not 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, you can supply a Mockito ArgumentCaptor<Message<?>> to the MockMessageHandler in a constructor argument. Each request message for the MockMessageHandler is captured by that ArgumentCaptor. During the test, you can use its getValue() and getAllValues() methods to verify and assert those request messages.

The MockIntegrationContext provides a substituteMessageHandlerFor() API that lets you replace the actual configured MessageHandler with a MockMessageHandler in the endpoint under test.

The following example shows a typical usage scenario:

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());
The regular MessageHandler mocking (or MockMessageHandler) has to be used even for a ReactiveStreamsConsumer with a ReactiveMessageHandler configuration.

See the MockIntegration and MockMessageHandler Javadoc for more information.

Other Resources

As well as exploring the test cases in the framework itself, the Spring Integration Samples repository has some sample applications specifically made to show 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.