測試支援
為非同步應用程式編寫整合測試必然比測試更簡單的應用程式複雜。當諸如 @RabbitListener 註解之類的抽象引入時,這會變得更加複雜。問題在於如何驗證在傳送訊息後,監聽器按預期接收了訊息。
框架本身有許多單元和整合測試。有些使用模擬物件,而另一些則使用帶有即時 RabbitMQ 代理的整合測試。您可以參考這些測試以獲取一些測試場景的思路。
Spring AMQP 1.6 版引入了 spring-rabbit-test jar,它為測試一些更復雜的場景提供了支援。預計該專案會隨著時間而擴充套件,但我們需要社群反饋來提出幫助測試所需的功能建議。請使用 JIRA 或 GitHub Issues 提供此類反饋。
@SpringRabbitTest
使用此註解將基礎設施 Bean 新增到 Spring 測試 ApplicationContext。在使用例如 @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<?> 實現
目前有兩個 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();
第二個是 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
使用 @RabbitListenerTest 註解您的一個 @Configuration 類會導致框架用一個名為 RabbitListenerTestHarness 的子類替換標準的 RabbitListenerAnnotationBeanPostProcessor(它還透過 @EnableRabbit 啟用 @RabbitListener 檢測)。
RabbitListenerTestHarness 透過兩種方式增強了監聽器。首先,它將監聽器包裝在一個 Mockito Spy 中,從而可以進行正常的 Mockito 存根和驗證操作。它還可以向監聽器新增一個 Advice,從而可以訪問引數、結果和任何丟擲的異常。您可以透過 @RabbitListenerTest 上的屬性控制啟用其中哪個(或兩者)。後者用於訪問較低級別的呼叫資料。它還支援阻塞測試執行緒,直到呼叫非同步監聽器。
final @RabbitListener 方法不能被監視或建議。此外,只有具有 id 屬性的監聽器才能被監視或建議。 |
考慮一些例子。
以下示例使用 spy
@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 | 將 harness 注入到測試用例中,以便我們可以訪問 spy。 |
| 2 | 獲取 spy 的引用,以便我們可以驗證它是否按預期呼叫。由於這是 send 和 receive 操作,因此無需暫停測試執行緒,因為它已經在 RabbitTemplate 中暫停等待回覆。 |
| 3 | 在這種情況下,我們只使用傳送操作,所以我們需要一個閉鎖來等待容器執行緒上對監聽器的非同步呼叫。我們使用一個 Answer<?> 實現來幫助完成此操作。重要:由於監聽器被監視的方式,使用 harness.getLatchAnswerFor() 來為 spy 獲取一個正確配置的答案非常重要。 |
| 4 | 配置 spy 以呼叫 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 | 將 harness 注入到測試用例中,以便我們可以訪問 spy。 |
| 2 | 使用 harness.getNextInvocationDataFor() 來檢索呼叫資料 - 在這種情況下,由於是請求/回覆場景,無需等待任何時間,因為測試執行緒已在 RabbitTemplate 中暫停等待結果。 |
| 3 | 然後我們可以驗證引數和結果是否符合預期。 |
| 4 | 這次我們需要一些時間來等待資料,因為它是一個容器執行緒上的非同步操作,我們需要暫停測試執行緒。 |
| 5 | 當監聽器丟擲異常時,它可在呼叫資料的 throwable 屬性中獲取。 |
當與 harness 一起使用自定義 Answer<?> 時,為了正常操作,此類答案應繼承 ForwardsInvocation 並從 harness (getDelegate("myListener")) 獲取實際監聽器(而不是 spy),並呼叫 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 處理。
該註解有三個屬性
-
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 連線(除非明確設定了管理 URI)。
BrokerRunningSupport 還提供了一個名為 setEnvironmentVariableOverrides 的 static 方法,您可以傳入一個包含這些變數的對映。它們會覆蓋系統環境變數。如果您希望在多個測試套件中使用不同的測試配置,這可能會很有用。重要:該方法必須在呼叫建立規則例項的任何 isRunning() 靜態方法之前呼叫。變數值將應用於此呼叫之後建立的所有例項。呼叫 clearEnvironmentVariableOverrides() 以將規則重置為使用預設值(包括任何實際的環境變數)。
在您的測試用例中,您可以在建立連線工廠時使用 RabbitAvailableCondition.getBrokerRunning();getConnectionFactory() 返回規則的 RabbitMQ ConnectionFactory。以下示例展示瞭如何操作
@Bean
public CachingConnectionFactory rabbitConnectionFactory() {
return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory());
}
此外,RabbitAvailableCondition 支援引數化測試建構函式和方法的引數解析。支援兩種引數型別
-
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 屬性中指定變數名稱。