Spring Cloud Stream provides support for testing your microservice applications without connecting to a messaging system.
You can do that by using the TestSupportBinder provided by the spring-cloud-stream-test-support library, which can be added as a test dependency to the application, as shown in the following example:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream-test-support</artifactId> <scope>test</scope> </dependency>
![]() | Note |
|---|---|
The |
The TestSupportBinder lets you interact with the bound channels and inspect any messages sent and received by the application.
For outbound message channels, the TestSupportBinder registers a single subscriber and retains the messages emitted by the application in a MessageCollector.
They can be retrieved during tests and have assertions made against them.
You can also send messages to inbound message channels so that the consumer application can consume the messages. The following example shows how to test both input and output channels on a processor:
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) public class ExampleTest { @Autowired private Processor processor; @Autowired private MessageCollector messageCollector; @Test @SuppressWarnings("unchecked") public void testWiring() { Message<String> message = new GenericMessage<>("hello"); processor.input().send(message); Message<String> received = (Message<String>) messageCollector.forChannel(processor.output()).poll(); assertThat(received.getPayload(), equalTo("hello world")); } @SpringBootApplication @EnableBinding(Processor.class) public static class MyProcessor { @Autowired private Processor channels; @Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT) public String transform(String in) { return in + " world"; } } }
In the preceding example, we create an application that has an input channel and an output channel, both bound through the Processor interface.
The bound interface is injected into the test so that we can have access to both channels.
We send a message on the input channel, and we use the MessageCollector provided by Spring Cloud Stream’s test support to capture that the message has been sent to the output channel as a result.
Once we have received the message, we can validate that the component functions correctly.
The intent behind the test binder superseding all the other binders on the classpath is to make it easy to test your applications without making changes to your production dependencies.
In some cases (for example, integration tests) it is useful to use the actual production binders instead, and that requires disabling the test binder autoconfiguration.
To do so, you can exclude the org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration class by using one of the Spring Boot autoconfiguration exclusion mechanisms, as shown in the following example:
@SpringBootApplication(exclude = TestSupportBinderAutoConfiguration.class) @EnableBinding(Processor.class) public static class MyProcessor { @Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT) public String transform(String in) { return in + " world"; } }
When autoconfiguration is disabled, the test binder is available on the classpath, and its defaultCandidate property is set to false so that it does not interfere with the regular user configuration. It can be referenced under the name, test, as shown in the following example:
spring.cloud.stream.defaultBinder=test
Current test binder was specifically designed to facilitate unit testing of the actual messaging components and thus bypasses some of the core functionality of the binder API. While such light-weight approach is sufficient for a lot of cases, it usually requires additional integration testing with real binders (e.g., Rabbit, Kafka etc).
To begin bridging the gap between unit and integration testing we’ve developed a new test binder which uses Spring Integration framework as an in-JVM Message Broker essentially giving you the best of both worlds - a real binder without the networking.
To enable Spring Integration Test Binder all you need is:
spring-cloud-stream-test-supportAdd required dependencies
Below is the example of the required Maven POM entries which could be easily retrofitted into Gradle.
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream</artifactId> <version>${spring.cloud.strea.version}</version> <type>test-jar</type> <scope>test</scope> <classifier>test-binder</classifier> </dependency> . . . <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <executions> <execution> <configuration> <includes> <include>**/integration/*</include> </includes> <classifier>test-binder</classifier> </configuration> <goals> <goal>test-jar</goal> </goals> </execution> </executions> </plugin> </plugins>
Remove the dependency for spring-cloud-stream-test-support
To avoid conflicts with the existing test binder you must eremove the following entry
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream-test-support</artifactId> <scope>test</scope> </dependency>
Now you can test your microservice as a simple unit test
@SpringBootApplication @EnableBinding(Processor.class) public class DemoTestBinderApplication { public static void main(String[] args) { SpringApplication.run(DemoTestBinderApplication.class, args); } @StreamListener(Processor.INPUT) @SendTo(Processor.OUTPUT) public String echo(String value) { return value; } } . . . @Test public void sampleTest() { ApplicationContext context = new SpringApplicationBuilder( TestChannelBinderConfiguration.class, DemoTestBinderApplication.class) .web(WebApplicationType.NONE).run(); InputDestination source = context.getBean(InputDestination.class); OutputDestination target = context.getBean(OutputDestination.class); source.send(new GenericMessage<byte[]>("hello".getBytes())); System.out.println("Result: " + new String(target.receive().getPayload())); }
In the above you simply create an ApplicationContext with your configuration (your application) while additionally supplying TestChannelBinderConfiguration
provided by the framework. Then you access InputDestination and OutputDestination beans to send/receive messages. In the context of this binder
InputDestination and OutputDestination emulate remote destinations such as Rabbit exchange/queue or Kafka topic.
In the future we plan to simplify the API.
![]() | Note |
|---|---|
In its current state Spring Integration Test Binder only supports the three bindings provided by the framework (Source, Processor, Sink) specifically to promote light-weight microservices architectures rather then general purpose messaging applications. |
Spring Integration Test Binder also allows you to write tests when working with PollableMessageSource (see Section 7.3.5, “Using Polled Consumers” for more details).
The important thing that needs to be understood though is that polling is not event-driven, and that PollableMessageSource is a strategy which exposes operation to produce (poll for) a Message (singular).
How often you poll or how many threads you use or where you’re polling from (message queue or file system) is entirely up to you;
In other words it is your responsibility to configure Poller or Threads or the actual source of Message. Luckily Spring has plenty of abstractions to configure exactly that.
Let’s look at the example:
@Test public void samplePollingTest() { ApplicationContext context = new SpringApplicationBuilder(SamplePolledConfiguration.class) .web(WebApplicationType.NONE) .run("--spring.jmx.enabled=false"); OutputDestination destination = context.getBean(OutputDestination.class); System.out.println("Message 1: " + new String(destination.receive().getPayload())); System.out.println("Message 2: " + new String(destination.receive().getPayload())); System.out.println("Message 3: " + new String(destination.receive().getPayload())); } @EnableBinding(SamplePolledConfiguration.PolledConsumer.class) @Import(TestChannelBinderConfiguration.class) @EnableAutoConfiguration public static class SamplePolledConfiguration { @Bean public ApplicationRunner poller(PollableMessageSource polledMessageSource, MessageChannel output, TaskExecutor taskScheduler) { return args -> { taskScheduler.execute(() -> { for (int i = 0; i < 3; i++) { try { if (!polledMessageSource.poll(m -> { String newPayload = ((String) m.getPayload()).toUpperCase(); output.send(new GenericMessage<>(newPayload)); })) { Thread.sleep(2000); } } catch (Exception e) { // handle failure } } }); }; } public static interface PolledConsumer extends Source { @Input PollableMessageSource pollableSource(); } }
The above (very rudimentary) example will produce 3 messages in 2 second intervals sending them to the output destination of Source
which this binder sends to OutputDestination where we retrieve them (for any assertions).
Currently it prints the following:
Message 1: POLLED DATA Message 2: POLLED DATA Message 3: POLLED DATA
As you can see the data is the same. That is because this binder defines a default implementation of the actual MessageSource - the source
from which the Messages are polled using poll() operation. While sufficient for most testing scenarios, there are cases where you may want
to define your own MessageSource. To do so simply configure a bean of type MessageSource in your test configuration providing your own
implementation of Message sourcing.
Here is the example:
@Bean public MessageSource<?> source() { return () -> new GenericMessage<>("My Own Data " + UUID.randomUUID()); }
rendering the following output;
Message 1: MY OWN DATA 1C180A91-E79F-494F-ABF4-BA3F993710DA Message 2: MY OWN DATA D8F3A477-5547-41B4-9434-E69DA7616FEE Message 3: MY OWN DATA 20BF2E64-7FF4-4CB6-A823-4053D30B5C74
![]() | Note |
|---|---|
DO NOT name this bean |