テストサポート
非同期アプリケーションの統合を作成することは、単純なアプリケーションをテストするよりも必然的に複雑になります。@RabbitListener アノテーションなどの抽象化が登場すると、これはより複雑になります。問題は、メッセージを送信した後、リスナーが期待どおりにメッセージを受信したことを確認する方法です。
フレームワーク自体には、多くの単体テストと統合テストがあります。モックを使用するものもあれば、ライブの RabbitMQ ブローカーとの統合テストを使用するものもあります。テストシナリオのアイデアについては、これらのテストを参照してください。
Spring AMQP バージョン 1.6 は、spring-rabbit-test jar を導入しました。これは、これらのより複雑なシナリオのいくつかをテストするためのサポートを提供します。このプロジェクトは時間の経過とともに拡大することが予想されますが、テストを支援するために必要な機能を提案するには、コミュニティからのフィードバックが必要です。このようなフィードバックを提供するには、JIRA (英語) または GitHub の課題 (英語) を使用してください。
@SpringRabbitTest
このアノテーションを使用して、Spring Test ApplicationContext にインフラストラクチャ Bean を追加します。たとえば @SpringBootTest を使用する場合は、Spring Boot の自動構成によって Bean が追加されるため、このアノテーションは必要ありません。
登録されている Bean は次のとおりです。
CachingConnectionFactory(autoConnectionFactory)。@RabbitEnabledが存在する場合、その接続ファクトリが使用されます。RabbitTemplate(autoRabbitTemplate)RabbitAdmin(autoRabbitAdmin)RabbitListenerContainerFactory(autoContainerFactory)
さらに、@EnableRabbit に関連付けられた Bean ( @RabbitListener をサポートするため) が追加されました。
@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 {
...
}
}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) {
...
}
}
@SpringJUnitConfig
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 | スパイへの参照を取得して、期待通りに呼び出されたかどうかを確認します。これは send および receive 操作であるため、テストスレッドはすでに RabbitTemplate で応答待ちのためサスペンドされているため、サスペンドする必要はありません。 |
| 3 | この場合、送信操作のみを使用しているため、コンテナースレッドでリスナーへの非同期呼び出しを待機するラッチが必要です。これを支援するために、答え <?> 実装の 1 つを使用します。重要: リスナーがスパイされる方法のため、harness.getLatchAnswerFor() を使用して、スパイに対して適切に構成された回答を取得することが重要です。 |
| 4 | Answer を呼び出すようにスパイを構成します。 |
次の例では、キャプチャーアドバイスを使用しています。
@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;
}
}
@SpringJUnitConfig
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 | ハーネスをテストケースに挿入して、スパイにアクセスできるようにします。 |
| 2 | harness.getNextInvocationDataFor() を使用して呼び出しデータを取得します。この場合はリクエスト / 応答のシナリオであるため、テストスレッドが RabbitTemplate で結果を待って中断されたため、しばらく待つ必要はありません。 |
| 3 | 次に、引数と結果が期待どおりであることを確認できます。 |
| 4 | 今回は、コンテナースレッドでの非同期操作であり、テストスレッドを一時停止する必要があるため、データを待つ時間が必要です。 |
| 5 | リスナーが例外をスローすると、呼び出しデータの throwable プロパティで使用できます。 |
ハーネスでカスタム Answer<?> を使用する場合、適切に動作するために、そのような回答は ForwardsInvocation をサブクラス化し、ハーネス (getDelegate("myListener")) から実際のリスナー (スパイではない) を取得し、super.answer(invocation) を呼び出す必要があります。例については、提供されている Mockito Answer<?> 実装ソースコードを参照してください。 |
TestRabbitTemplate を使用する
TestRabbitTemplate は、ブローカーを必要とせずにいくつかの基本的な統合テストを実行するために提供されています。これをテストケースに @Bean として追加すると、@Bean または <bean/> として宣言されているか、@RabbitListener アノテーションを使用しているかに関係なく、コンテキスト内のすべてのリスナーコンテナーが検出されます。現在、キュー名によるルーティングのみをサポートしています。テンプレートはコンテナーからメッセージリスナーを抽出し、テストスレッドで直接呼び出します。応答を返すリスナーでは、リクエストと応答のメッセージング (sendAndReceive メソッド) がサポートされています。
次のテストケースでは、テンプレートを使用します。
@SpringJUnitConfig
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;
}
}JUnit5 条件
バージョン 2.0.2 では、JUnit5 のサポートが導入されました。
@RabbitAvailable アノテーションの使用
カスタム JUnit 5 @RabbitAvailable アノテーションは、RabbitAvailableCondition によって処理されます。
アノテーションには次の 3 つのプロパティがあります。
queues: 各テストの前に宣言 (およびパージ) され、すべてのテストが完了すると削除されるキューの配列。management: テストでブローカーに管理プラグインをインストールする必要がある場合は、これをtrueに設定します。purgeAfterEach: (バージョン 2.2 以降)true(デフォルト) の場合、queuesはテスト間でパージされます。
ブローカーが利用可能かどうかを確認し、利用できない場合はテストをスキップするために使用されます。
夜間 CI ビルドなど、ブローカーがない場合にテストを失敗させたい場合があります。実行時に BrokerRunningSupport を無効にするには、環境変数 RABBITMQ_SERVER_REQUIRED を true に設定します。
ホスト名などのブローカーのプロパティを、setter または環境変数で上書きできます。
次の例は、setter でプロパティをオーバーライドする方法を示しています。
@RabbitAvailable
...
@BeforeAll
static void setup() {
RabbitAvailableCondition.getBrokerRunning().setHostName("10.0.0.1");
}
@AfterAll
static void tearDown() {
RabbitAvailableCondition.getBrokerRunning().removeTestQueues("some.other.queue.too");
}次の環境変数を設定して、プロパティをオーバーライドすることもできます。
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 が明示的に設定されていない場合)。
BrokerRunningSupport は、これらの変数を含むマップを渡すことができる setEnvironmentVariableOverrides と呼ばれる static メソッドも提供します。システム環境変数をオーバーライドします。これは、複数のテストスイートのテストに異なる構成を使用する場合に便利です。重要: このメソッドは、ルールインスタンスを作成する isRunning() 静的メソッドを呼び出す前に呼び出す必要があります。変数値は、この呼び出し後に作成されたすべてのインスタンスに適用されます。clearEnvironmentVariableOverrides() を呼び出して、デフォルト (実際の環境変数を含む) を使用するようにルールをリセットします。
テストケースでは、接続ファクトリを作成するときに RabbitAvailableCondition.getBrokerRunning() を使用できます。getConnectionFactory() は、ルールの RabbitMQ ConnectionFactory を返します。次の例は、その方法を示しています。
@Bean
public CachingConnectionFactory rabbitConnectionFactory() {
return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory());
} さらに、RabbitAvailableCondition は、パラメーター化されたテストコンストラクターおよびメソッドの引数解決をサポートします。次の 2 つの引数型がサポートされています。
BrokerRunningSupport: インスタンスConnectionFactory:BrokerRunningSupportインスタンスの 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() という静的メソッドを介して条件の接続ファクトリへの参照を取得できます。
次のテストはフレームワークからのもので、使用箇所を示しています。
@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 アノテーションの使用
@LongRunning アノテーションは、環境変数(またはシステムプロパティ)が true に設定されていない限り、テストをスキップします。次の例は、その使用方法を示しています。
@RabbitAvailable(queues = SimpleMessageListenerContainerLongTests.QUEUE)
@LongRunning
public class SimpleMessageListenerContainerLongTests {
public static final String QUEUE = "SimpleMessageListenerContainerLongTests.queue";
...
} デフォルトでは、変数は RUN_LONG_INTEGRATION_TESTS ですが、アノテーションの value 属性で変数名を指定できます。