テストサポート
非同期アプリケーションの統合を作成することは、単純なアプリケーションをテストするよりも必然的に複雑になります。@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
をサポートするため) が追加されました。
@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() を使用して、スパイに対して適切に構成された回答を取得することが重要です。 |
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;
}
}
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
メソッド) がサポートされています。
次のテストケースでは、テンプレートを使用します。
@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 4BrokerRunning
インスタンスでした)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()
という静的メソッドを介して条件の接続ファクトリへの参照を取得できます。
バージョン 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
属性で変数名を指定できます。