Testing without brokers

Due to a more strict channel name validation introduced in 2.x, the API has changed in 2.1.0.

It’s not rare to have to test your application but deploying the infrastructure can be cumbersome. While Docker or Test Containers have improved the testing experience, you may want to mock this infrastructure.

SmallRye Reactive Messaging proposes an in-memory connector for this exact purpose. It allows switching the connector used for a channel with an in-memory connector. This in-memory connector provides a way to send messages to incoming channels, or check the received messages for outgoing channels.

To use the in-memory connector, you need to add the following dependency to your project:

<dependency>
  <groupId>io.smallrye.reactive</groupId>
  <artifactId>smallrye-reactive-messaging-in-memory</artifactId>
  <version>3.10.1</version>
  <scope>test</scope>
</dependency>

Then, in a test, you can do something like:

package testing;

import io.smallrye.reactive.messaging.connectors.InMemoryConnector;
import io.smallrye.reactive.messaging.connectors.InMemorySource;
import io.smallrye.reactive.messaging.connectors.InMemorySink;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import javax.enterprise.inject.Any;
import javax.inject.Inject;

public class MyTest {

    // 1. Switch the channels to the in-memory connector:
    @BeforeAll
    public static void switchMyChannels() {
        InMemoryConnector.switchIncomingChannelsToInMemory("prices");
        InMemoryConnector.switchOutgoingChannelsToInMemory("processed-prices");
    }

    // 2. Don't forget to reset the channel after the tests:
    @AfterAll
    public static void revertMyChannels() {
        InMemoryConnector.clear();
    }

    // 3. Inject the in-memory connector in your test,
    // or use the bean manager to retrieve the instance
    @Inject @Any
    InMemoryConnector connector;

    @Test
    void test() {
        // 4. Retrieves the in-memory source to send message
        InMemorySource<Integer> prices = connector.source("prices");
        // 5. Retrieves the in-memory sink to check what is received
        InMemorySink<Integer> results = connector.sink("processed-prices");

        // 6. Send fake messages:
        prices.send(1);
        prices.send(2);
        prices.send(3);

        // 7. Check you have receives the expected messages
        Assertions.assertEquals(3, results.received().size());
    }
}

When switching a channel to the in-memory connector, all the configuration properties are ignored.

This connector has been designed for testing purpose only.

The switch methods return Map<String, String> instances containing the set properties. While these system properties are already set, you can retrieve them and pass them around, for example if you need to start an external process with these properties:

public Map<String, String> start() {
    Map<String, String> env = new HashMap<>();
    env.putAll(InMemoryConnector.switchIncomingChannelsToInMemory("prices"));
    env.putAll(InMemoryConnector.switchOutgoingChannelsToInMemory("my-data-stream"));
    return env;
}

public void stop() {
    InMemoryConnector.clear();
}
The in-memory connector support the broadcast and merge attributes. So, if your connector is configured with broadcast: true, the connector broadcasts the messages to all the channel consumers. If your connector is configured with merge:true, the connector receives all the messages sent to the mapped channel even when coming from multiple producers.