テストサポート

非同期アプリケーションの統合を作成することは、単純なアプリケーションをテストするよりも必然的に複雑になります。@RabbitListener アノテーションなどの抽象化が登場すると、これはより複雑になります。問題は、メッセージを送信した後、リスナーが期待どおりにメッセージを受信したことを確認する方法です。

フレームワーク自体には、多くの単体テストと統合テストがあります。モックを使用するものもあれば、ライブの RabbitMQ ブローカーとの統合テストを使用するものもあります。テストシナリオのアイデアについては、これらのテストを参照してください。

Spring AMQP バージョン 1.6 は、spring-rabbit-test jar を導入しました。これは、これらのより複雑なシナリオのいくつかをテストするためのサポートを提供します。このプロジェクトは時間の経過とともに拡大することが予想されますが、テストを支援するために必要な機能を提案するには、コミュニティからのフィードバックが必要です。このようなフィードバックを提供するには、JIRA (英語) または GitHub の課題 (英語) を使用してください。

@SpringRabbitTest

このアノテーションを使用して、インフラストラクチャ Bean を Spring Test ApplicationContext に追加します。Spring Boot の自動構成によって Bean が追加されるため、たとえば @SpringBootTest を使用する場合、これは必要ありません。

登録されている Bean は次のとおりです。

  • CachingConnectionFactory (autoConnectionFactory)。@RabbitEnabled が存在する場合、その接続ファクトリが使用されます。

  • RabbitTemplate (autoRabbitTemplate)

  • RabbitAdmin (autoRabbitAdmin)

  • RabbitListenerContainerFactory (autoContainerFactory)

さらに、@EnableRabbit に関連付けられた Bean ( @RabbitListener をサポートするため) が追加されました。

Junit5 の例
@SpringJUnitConfig
@SpringRabbitTest
public class MyRabbitTests {

	@Autowired
	private RabbitTemplate template;

	@Autowired
	private RabbitAdmin admin;

	@Autowired
	private RabbitListenerEndpointRegistry registry;

	@Test
	void test() {
        ...
	}

	@Configuration
	public static class Config {

        ...

	}

}

JUnit4 では、@SpringJUnitConfig を @RunWith(SpringRunnner.class) に置き換えます。

Mockito Answer<?> 実装

現在、テストに役立つ 2 つの Answer<?> 実装があります。

最初の LatchCountDownAndCallRealMethodAnswer は、null を返し、ラッチをカウントダウンする Answer<Void> を提供します。次の例は、LatchCountDownAndCallRealMethodAnswer の使用方法を示しています。

LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("myListener", 2);
doAnswer(answer)
    .when(listener).foo(anyString(), anyString());

...

assertThat(answer.await(10)).isTrue();

2 番目の LambdaAnswer<T> は、オプションで実際のメソッドを呼び出すメカニズムを提供し、InvocationOnMock と結果 (存在する場合) に基づいてカスタム結果を返す機会を提供します。

次の POJO を検討してください。

public class Thing {

    public String thing(String thing) {
        return thing.toUpperCase();
    }

}

次のクラスは、Thing POJO をテストします。

Thing thing = spy(new Thing());

doAnswer(new LambdaAnswer<String>(true, (i, r) -> r + r))
    .when(thing).thing(anyString());
assertEquals("THINGTHING", thing.thing("thing"));

doAnswer(new LambdaAnswer<String>(true, (i, r) -> r + i.getArguments()[0]))
    .when(thing).thing(anyString());
assertEquals("THINGthing", thing.thing("thing"));

doAnswer(new LambdaAnswer<String>(false, (i, r) ->
    "" + i.getArguments()[0] + i.getArguments()[0])).when(thing).thing(anyString());
assertEquals("thingthing", thing.thing("thing"));

バージョン 2.2.3 以降、回答は、テスト対象のメソッドによってスローされたすべての例外をキャプチャーします。それらへの参照を取得するには、answer.getExceptions() を使用します。

@RabbitListenerTest および RabbitListenerTestHarness と組み合わせて使用する場合は、harness.getLambdaAnswerFor("listenerId", true, …​) を使用して、リスナーに対して適切に構築された回答を取得します。

@RabbitListenerTest および RabbitListenerTestHarness

@Configuration クラスの 1 つに @RabbitListenerTest のアノテーションを付けると、フレームワークは標準の RabbitListenerAnnotationBeanPostProcessor を RabbitListenerTestHarness というサブクラスに置き換えます (これにより、@EnableRabbit による @RabbitListener 検出も有効になります)。

RabbitListenerTestHarness は、2 つの方法でリスナーを強化します。まず、リスナーを Mockito Spy でラップし、通常の Mockito スタブと検証操作を有効にします。また、リスナーに Advice を追加して、引数、結果、スローされる例外へのアクセスを有効にすることもできます。@RabbitListenerTest の属性を使用して、これらのどちら (または両方) を有効にするかを制御できます。後者は、呼び出しに関する下位レベルのデータにアクセスするために提供されます。また、非同期リスナーが呼び出されるまでテストスレッドをブロックすることもサポートしています。

final @RabbitListener メソッドはスパイまたはアドバイスできません。また、id 属性を持つリスナーのみがスパイまたはアドバイスを受けることができます。

いくつかの例を考えてみましょう。

次の例では、スパイを使用しています。

@Configuration
@RabbitListenerTest
public class Config {

    @Bean
    public Listener listener() {
        return new Listener();
    }

    ...

}

public class Listener {

    @RabbitListener(id="foo", queues="#{queue1.name}")
    public String foo(String foo) {
        return foo.toUpperCase();
    }

    @RabbitListener(id="bar", queues="#{queue2.name}")
    public void foo(@Payload String foo, @Header("amqp_receivedRoutingKey") String rk) {
        ...
    }

}

public class MyTests {

    @Autowired
    private RabbitListenerTestHarness harness; (1)

    @Test
    public void testTwoWay() throws Exception {
        assertEquals("FOO", this.rabbitTemplate.convertSendAndReceive(this.queue1.getName(), "foo"));

        Listener listener = this.harness.getSpy("foo"); (2)
        assertNotNull(listener);
        verify(listener).foo("foo");
    }

    @Test
    public void testOneWay() throws Exception {
        Listener listener = this.harness.getSpy("bar");
        assertNotNull(listener);

        LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("bar", 2); (3)
        doAnswer(answer).when(listener).foo(anyString(), anyString()); (4)

        this.rabbitTemplate.convertAndSend(this.queue2.getName(), "bar");
        this.rabbitTemplate.convertAndSend(this.queue2.getName(), "baz");

        assertTrue(answer.await(10));
        verify(listener).foo("bar", this.queue2.getName());
        verify(listener).foo("baz", this.queue2.getName());
    }

}
1 ハーネスをテストケースに挿入して、スパイにアクセスできるようにします。
2 スパイへの参照を取得して、期待どおりに呼び出されたことを確認できるようにします。これは送信と受信の操作であるため、テストスレッドを中断する必要はありません。テストスレッドは応答を待っている RabbitTemplate ですでに中断されているからです。
3 この場合、送信操作のみを使用しているため、コンテナースレッドでリスナーへの非同期呼び出しを待機するラッチが必要です。これを支援するために、答え <?> 実装の 1 つを使用します。重要: リスナーがスパイされる方法のため、harness.getLatchAnswerFor() を使用して、スパイに対して適切に構成された回答を取得することが重要です。
4Answer を呼び出すようにスパイを構成します。

次の例では、キャプチャーアドバイスを使用しています。

@Configuration
@ComponentScan
@RabbitListenerTest(spy = false, capture = true)
public class Config {

}

@Service
public class Listener {

    private boolean failed;

    @RabbitListener(id="foo", queues="#{queue1.name}")
    public String foo(String foo) {
        return foo.toUpperCase();
    }

    @RabbitListener(id="bar", queues="#{queue2.name}")
    public void foo(@Payload String foo, @Header("amqp_receivedRoutingKey") String rk) {
        if (!failed && foo.equals("ex")) {
            failed = true;
            throw new RuntimeException(foo);
        }
        failed = false;
    }

}

public class MyTests {

    @Autowired
    private RabbitListenerTestHarness harness; (1)

    @Test
    public void testTwoWay() throws Exception {
        assertEquals("FOO", this.rabbitTemplate.convertSendAndReceive(this.queue1.getName(), "foo"));

        InvocationData invocationData =
            this.harness.getNextInvocationDataFor("foo", 0, TimeUnit.SECONDS); (2)
        assertThat(invocationData.getArguments()[0], equalTo("foo"));     (3)
        assertThat((String) invocationData.getResult(), equalTo("FOO"));
    }

    @Test
    public void testOneWay() throws Exception {
        this.rabbitTemplate.convertAndSend(this.queue2.getName(), "bar");
        this.rabbitTemplate.convertAndSend(this.queue2.getName(), "baz");
        this.rabbitTemplate.convertAndSend(this.queue2.getName(), "ex");

        InvocationData invocationData =
            this.harness.getNextInvocationDataFor("bar", 10, TimeUnit.SECONDS); (4)
        Object[] args = invocationData.getArguments();
        assertThat((String) args[0], equalTo("bar"));
        assertThat((String) args[1], equalTo(queue2.getName()));

        invocationData = this.harness.getNextInvocationDataFor("bar", 10, TimeUnit.SECONDS);
        args = invocationData.getArguments();
        assertThat((String) args[0], equalTo("baz"));

        invocationData = this.harness.getNextInvocationDataFor("bar", 10, TimeUnit.SECONDS);
        args = invocationData.getArguments();
        assertThat((String) args[0], equalTo("ex"));
        assertEquals("ex", invocationData.getThrowable().getMessage()); (5)
    }

}
1 ハーネスをテストケースに挿入して、スパイにアクセスできるようにします。
2harness.getNextInvocationDataFor() を使用して呼び出しデータを取得します。この場合はリクエスト / 応答のシナリオであるため、テストスレッドが RabbitTemplate で結果を待って中断されたため、しばらく待つ必要はありません。
3 次に、引数と結果が期待どおりであることを確認できます。
4 今回は、コンテナースレッドでの非同期操作であり、テストスレッドを一時停止する必要があるため、データを待つ時間が必要です。
5 リスナーが例外をスローすると、呼び出しデータの throwable プロパティで使用できます。
ハーネスでカスタム Answer<?> を使用する場合、適切に動作するために、そのような回答は ForwardsInvocation をサブクラス化し、ハーネス (getDelegate("myListener")) から実際のリスナー (スパイではない) を取得し、super.answer(invocation) を呼び出す必要があります。例については、提供されている Mockito Answer<?> 実装ソースコードを参照してください。

TestRabbitTemplate を使用する

TestRabbitTemplate は、ブローカーを必要とせずにいくつかの基本的な統合テストを実行するために提供されています。これをテストケースに @Bean として追加すると、@Bean または <bean/> として宣言されているか、@RabbitListener アノテーションを使用しているかに関係なく、コンテキスト内のすべてのリスナーコンテナーが検出されます。現在、キュー名によるルーティングのみをサポートしています。テンプレートはコンテナーからメッセージリスナーを抽出し、テストスレッドで直接呼び出します。応答を返すリスナーでは、リクエストと応答のメッセージング (sendAndReceive メソッド) がサポートされています。

次のテストケースでは、テンプレートを使用します。

@RunWith(SpringRunner.class)
public class TestRabbitTemplateTests {

    @Autowired
    private TestRabbitTemplate template;

    @Autowired
    private Config config;

    @Test
    public void testSimpleSends() {
        this.template.convertAndSend("foo", "hello1");
        assertThat(this.config.fooIn, equalTo("foo:hello1"));
        this.template.convertAndSend("bar", "hello2");
        assertThat(this.config.barIn, equalTo("bar:hello2"));
        assertThat(this.config.smlc1In, equalTo("smlc1:"));
        this.template.convertAndSend("foo", "hello3");
        assertThat(this.config.fooIn, equalTo("foo:hello1"));
        this.template.convertAndSend("bar", "hello4");
        assertThat(this.config.barIn, equalTo("bar:hello2"));
        assertThat(this.config.smlc1In, equalTo("smlc1:hello3hello4"));

        this.template.setBroadcast(true);
        this.template.convertAndSend("foo", "hello5");
        assertThat(this.config.fooIn, equalTo("foo:hello1foo:hello5"));
        this.template.convertAndSend("bar", "hello6");
        assertThat(this.config.barIn, equalTo("bar:hello2bar:hello6"));
        assertThat(this.config.smlc1In, equalTo("smlc1:hello3hello4hello5hello6"));
    }

    @Test
    public void testSendAndReceive() {
        assertThat(this.template.convertSendAndReceive("baz", "hello"), equalTo("baz:hello"));
    }
    @Configuration
    @EnableRabbit
    public static class Config {

        public String fooIn = "";

        public String barIn = "";

        public String smlc1In = "smlc1:";

        @Bean
        public TestRabbitTemplate template() throws IOException {
            return new TestRabbitTemplate(connectionFactory());
        }

        @Bean
        public ConnectionFactory connectionFactory() throws IOException {
            ConnectionFactory factory = mock(ConnectionFactory.class);
            Connection connection = mock(Connection.class);
            Channel channel = mock(Channel.class);
            willReturn(connection).given(factory).createConnection();
            willReturn(channel).given(connection).createChannel(anyBoolean());
            given(channel.isOpen()).willReturn(true);
            return factory;
        }

        @Bean
        public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() throws IOException {
            SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
            factory.setConnectionFactory(connectionFactory());
            return factory;
        }

        @RabbitListener(queues = "foo")
        public void foo(String in) {
            this.fooIn += "foo:" + in;
        }

        @RabbitListener(queues = "bar")
        public void bar(String in) {
            this.barIn += "bar:" + in;
        }

        @RabbitListener(queues = "baz")
        public String baz(String in) {
            return "baz:" + in;
        }

        @Bean
        public SimpleMessageListenerContainer smlc1() throws IOException {
            SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory());
            container.setQueueNames("foo", "bar");
            container.setMessageListener(new MessageListenerAdapter(new Object() {

                public void handleMessage(String in) {
                    smlc1In += in;
                }

            }));
            return container;
        }

    }

}

JUnit4 @Rules

Spring AMQP バージョン 1.7 以降では、spring-rabbit-junit と呼ばれる追加の jar が提供されます。この jar には、JUnit4 テストの実行時に使用するユーティリティ @Rule インスタンスがいくつか含まれています。JUnit5 のテストについては、JUnit5 条件を参照してください。

BrokerRunning を使用する

BrokerRunning は、ブローカーが実行されていない場合 (デフォルトでは localhost 上) にテストを成功させるメカニズムを提供します。

また、キューを初期化して空にし、キューと交換を削除するためのユーティリティメソッドもあります。

次の例は、その使用箇所を示しています。

@ClassRule
public static BrokerRunning brokerRunning = BrokerRunning.isRunningWithEmptyQueues("foo", "bar");

@AfterClass
public static void tearDown() {
    brokerRunning.removeTestQueues("some.other.queue.too") // removes foo, bar as well
}

ブローカーで管理プラグインが有効になっていることを確認する isBrokerAndManagementRunning() など、いくつかの isRunning…​ 静的メソッドがあります。

ルールの構成

夜間の CI ビルドなど、ブローカーがない場合にテストを失敗させたい場合があります。実行時にルールを無効にするには、RABBITMQ_SERVER_REQUIRED という環境変数を true に設定します。

setter または環境変数のいずれかを使用して、ホスト名などのブローカープロパティをオーバーライドできます。

次の例は、setter でプロパティをオーバーライドする方法を示しています。

@ClassRule
public static BrokerRunning brokerRunning = BrokerRunning.isRunningWithEmptyQueues("foo", "bar");

static {
    brokerRunning.setHostName("10.0.0.1")
}

@AfterClass
public static void tearDown() {
    brokerRunning.removeTestQueues("some.other.queue.too") // removes foo, bar as well
}

次の環境変数を設定して、プロパティをオーバーライドすることもできます。

public static final String BROKER_ADMIN_URI = "RABBITMQ_TEST_ADMIN_URI";
public static final String BROKER_HOSTNAME = "RABBITMQ_TEST_HOSTNAME";
public static final String BROKER_PORT = "RABBITMQ_TEST_PORT";
public static final String BROKER_USER = "RABBITMQ_TEST_USER";
public static final String BROKER_PW = "RABBITMQ_TEST_PASSWORD";
public static final String BROKER_ADMIN_USER = "RABBITMQ_TEST_ADMIN_USER";
public static final String BROKER_ADMIN_PW = "RABBITMQ_TEST_ADMIN_PASSWORD";

これらの環境変数は、デフォルト設定 (amqp の場合は localhost:5672、管理 REST API の場合は localhost:15672/api/ ) をオーバーライドします。

ホスト名を変更すると、amqp および management REST API 接続の両方に影響します (admin uri が明示的に設定されていない場合)。

BrokerRunning は、これらの変数を含むマップを渡すことができる setEnvironmentVariableOverrides と呼ばれる static メソッドも提供します。システム環境変数をオーバーライドします。これは、複数のテストスイートのテストに異なる構成を使用する場合に便利です。重要: このメソッドは、ルールインスタンスを作成する isRunning() 静的メソッドを呼び出す前に呼び出す必要があります。変数値は、この呼び出し後に作成されたすべてのインスタンスに適用されます。clearEnvironmentVariableOverrides() を呼び出して、デフォルト (実際の環境変数を含む) を使用するようにルールをリセットします。

テストケースでは、接続ファクトリを作成するときに brokerRunning を使用できます。getConnectionFactory() は、ルールの RabbitMQ ConnectionFactory を返します。次の例は、その方法を示しています。

@Bean
public CachingConnectionFactory rabbitConnectionFactory() {
    return new CachingConnectionFactory(brokerRunning.getConnectionFactory());
}

LongRunningIntegrationTest を使用する

LongRunningIntegrationTest は、実行時間の長いテストを無効にするルールです。これを開発者システムで使用したい場合がありますが、夜間の CI ビルドなどではルールが無効になっていることを確認してください。

次の例は、その使用箇所を示しています。

@Rule
public LongRunningIntegrationTest longTests = new LongRunningIntegrationTest();

実行時にルールを無効にするには、RUN_LONG_INTEGRATION_TESTS という環境変数を true に設定します。

JUnit5 条件

バージョン 2.0.2 では、JUnit5 のサポートが導入されました。

@RabbitAvailable アノテーションの使用

このクラスレベルのアノテーションは、JUnit4 @Rules で説明されている BrokerRunning @Rule に似ています。RabbitAvailableCondition によって処理されます。

アノテーションには次の 3 つのプロパティがあります。

  • queues: 各テストの前に宣言 (およびパージ) され、すべてのテストが完了すると削除されるキューの配列。

  • management: テストでブローカーに管理プラグインをインストールする必要がある場合は、これを true に設定します。

  • purgeAfterEach: (バージョン 2.2 以降) true (デフォルト) の場合、queues はテスト間でパージされます。

ブローカーが使用可能かどうかを確認し、そうでない場合はテストをスキップするために使用されます。ルールの構成で説明したように、RABBITMQ_SERVER_REQUIRED という環境変数が true の場合、ブローカーが存在しない場合、テストはすぐに失敗します。ルールの構成に従って、環境変数を使用して条件を構成できます。

さらに、RabbitAvailableCondition は、パラメーター化されたテストコンストラクターおよびメソッドの引数解決をサポートします。次の 2 つの引数型がサポートされています。

  • BrokerRunningSupport: インスタンス (2.2 より前は、これは JUnit 4 BrokerRunning インスタンスでした)

  • ConnectionFactoryBrokerRunningSupport インスタンスの RabbitMQ 接続ファクトリ

次の例は、両方を示しています。

@RabbitAvailable(queues = "rabbitAvailableTests.queue")
public class RabbitAvailableCTORInjectionTests {

    private final ConnectionFactory connectionFactory;

    public RabbitAvailableCTORInjectionTests(BrokerRunningSupport brokerRunning) {
        this.connectionFactory = brokerRunning.getConnectionFactory();
    }

    @Test
    public void test(ConnectionFactory cf) throws Exception {
        assertSame(cf, this.connectionFactory);
        Connection conn = this.connectionFactory.newConnection();
        Channel channel = conn.createChannel();
        DeclareOk declareOk = channel.queueDeclarePassive("rabbitAvailableTests.queue");
        assertEquals(0, declareOk.getConsumerCount());
        channel.close();
        conn.close();
    }

}

上記のテストはフレームワーク自体にあり、引数の挿入と、条件によってキューが適切に作成されたことを確認します。

実際のユーザーテストは次のようになります。

@RabbitAvailable(queues = "rabbitAvailableTests.queue")
public class RabbitAvailableCTORInjectionTests {

    private final CachingConnectionFactory connectionFactory;

    public RabbitAvailableCTORInjectionTests(BrokerRunningSupport brokerRunning) {
        this.connectionFactory =
            new CachingConnectionFactory(brokerRunning.getConnectionFactory());
    }

    @Test
    public void test() throws Exception {
        RabbitTemplate template = new RabbitTemplate(this.connectionFactory);
        ...
    }
}

テストクラス内で Spring アノテーションアプリケーションコンテキストを使用すると、RabbitAvailableCondition.getBrokerRunning() という静的メソッドを介して条件の接続ファクトリへの参照を取得できます。

バージョン 2.2 以降、getBrokerRunning() は BrokerRunningSupport オブジェクトを返します。以前は、JUnit 4 BrokerRunnning インスタンスが返されていました。新しいクラスには BrokerRunning と同じ API があります。

次のテストはフレームワークからのもので、使用箇所を示しています。

@RabbitAvailable(queues = {
        RabbitTemplateMPPIntegrationTests.QUEUE,
        RabbitTemplateMPPIntegrationTests.REPLIES })
@SpringJUnitConfig
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class RabbitTemplateMPPIntegrationTests {

    public static final String QUEUE = "mpp.tests";

    public static final String REPLIES = "mpp.tests.replies";

    @Autowired
    private RabbitTemplate template;

    @Autowired
    private Config config;

    @Test
    public void test() {

        ...

    }

    @Configuration
    @EnableRabbit
    public static class Config {

        @Bean
        public CachingConnectionFactory cf() {
            return new CachingConnectionFactory(RabbitAvailableCondition
                    .getBrokerRunning()
                    .getConnectionFactory());
        }

        @Bean
        public RabbitTemplate template() {

            ...

        }

        @Bean
        public SimpleRabbitListenerContainerFactory
                            rabbitListenerContainerFactory() {

            ...

        }

        @RabbitListener(queues = QUEUE)
        public byte[] foo(byte[] in) {
            return in;
        }

    }

}

@LongRunning アノテーションの使用

LongRunningIntegrationTest JUnit4 @Rule と同様に、このアノテーションにより、環境変数 (またはシステムプロパティ) が true に設定されていない限り、テストがスキップされます。次の例は、その使用方法を示しています。

@RabbitAvailable(queues = SimpleMessageListenerContainerLongTests.QUEUE)
@LongRunning
public class SimpleMessageListenerContainerLongTests {

    public static final String QUEUE = "SimpleMessageListenerContainerLongTests.queue";

...

}

デフォルトでは、変数は RUN_LONG_INTEGRATION_TESTS ですが、アノテーションの value 属性で変数名を指定できます。