測試支援
為非同步應用程式撰寫整合程式碼必然比測試更簡單的應用程式更複雜。當諸如 @RabbitListener
註解之類的抽象概念進入畫面時,這會變得更加複雜。問題是如何驗證在傳送訊息後,監聽器是否如預期接收到訊息。
框架本身有許多單元和整合測試。有些使用 Mock 物件,而另一些則使用與即時 RabbitMQ Broker 的整合測試。您可以參考這些測試,以獲得一些測試情境的想法。
Spring AMQP 1.6 版引入了 spring-rabbit-test
jar 檔,它為測試其中一些更複雜的情境提供了支援。預計此專案將隨著時間擴展,但我們需要社群回饋,以針對幫助測試所需的功能提出建議。請使用 JIRA 或 GitHub Issues 提供此類回饋。
@SpringRabbitTest
使用此註解將基礎結構 Bean 新增至 Spring 測試 ApplicationContext
。當使用 @SpringBootTest
等時,這不是必要的,因為 Spring Boot 的自動組態將新增 Bean。
已註冊的 Bean 為
-
CachingConnectionFactory
(autoConnectionFactory
)。如果存在@RabbitEnabled
,則會使用其連線 Factory。 -
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<?>
實作
目前有兩個 Answer<?>
實作可協助進行測試。
第一個 LatchCountDownAndCallRealMethodAnswer
提供了 Answer<Void>
,它會回傳 null
並倒數計時閂鎖。以下範例示範如何使用 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 會擷取受測方法擲回的任何例外。使用 answer.getExceptions()
取得它們的參考。
當與 @RabbitListenerTest
和 RabbitListenerTestHarness
結合使用時,使用 harness.getLambdaAnswerFor("listenerId", true, …)
來取得監聽器的正確建構 Answer。
@RabbitListenerTest
和 RabbitListenerTestHarness
使用 @RabbitListenerTest
註解您的其中一個 @Configuration
類別,會導致框架將標準 RabbitListenerAnnotationBeanPostProcessor
替換為名為 RabbitListenerTestHarness
的子類別(它也透過 @EnableRabbit
啟用 @RabbitListener
偵測)。
RabbitListenerTestHarness
以兩種方式增強監聽器。首先,它將監聽器包裝在 Mockito Spy
中,從而啟用正常的 Mockito
Stubbing 和驗證作業。它還可以將 Advice
新增至監聽器,從而允許存取引數、結果和擲回的任何例外。您可以使用 @RabbitListenerTest
上的屬性來控制啟用哪些(或兩者)。後者是為了存取有關調用的較低層級資料而提供的。它也支援封鎖測試執行緒,直到呼叫非同步監聽器。
final @RabbitListener 方法無法被 Spy 或 Advice。此外,只有具有 id 屬性的監聽器才能被 Spy 或 Advice。 |
考慮一些範例。
以下範例使用 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) {
...
}
}
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 的參考,以便我們可以驗證它是否如預期般被調用。由於這是傳送和接收作業,因此無需暫停測試執行緒,因為它已在 RabbitTemplate 中暫停,等待回覆。 |
3 | 在本例中,我們僅使用傳送作業,因此我們需要閂鎖來等待容器執行緒上對監聽器的非同步呼叫。我們使用其中一個 Answer<?> 實作來協助完成此操作。重要事項:由於 Spy 監聽器的方式,因此務必使用 harness.getLatchAnswerFor() 來取得 Spy 的正確設定 Answer。 |
4 | 將 Spy 設定為調用 Answer 。 |
以下範例使用擷取 Advice
@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 | 將 Harness 注入測試案例中,以便我們可以存取 Spy。 |
2 | 使用 harness.getNextInvocationDataFor() 來擷取調用資料 - 在本例中,由於它是請求/回覆情境,因此無需等待任何時間,因為測試執行緒已在 RabbitTemplate 中暫停,等待結果。 |
3 | 然後我們可以驗證引數和結果是否如預期。 |
4 | 這次我們需要一些時間來等待資料,因為它是容器執行緒上的非同步作業,並且我們需要暫停測試執行緒。 |
5 | 當監聽器擲回例外時,它會在調用資料的 throwable 屬性中提供。 |
當搭配 Harness 使用自訂 Answer<?> s 時,為了正確運作,此類 Answer 應子類別化 ForwardsInvocation 並從 Harness (getDelegate("myListener") ) 取得實際監聽器(而非 Spy),並呼叫 super.answer(invocation) 。請參閱提供的 Mockito Answer<?> 實作 原始程式碼以取得範例。 |
使用 TestRabbitTemplate
提供 TestRabbitTemplate
是為了在不需要 Broker 的情況下執行一些基本整合測試。當您將其作為 @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 版及更高版本提供了一個額外的 jar 檔,名為 spring-rabbit-junit
。此 jar 檔包含幾個公用程式 @Rule
實例,用於在執行 JUnit4 測試時使用。請參閱 JUnit5 條件 以取得 JUnit5 測試。
使用 BrokerRunning
BrokerRunning
提供了一種機制,讓測試在 Broker 未執行時(預設情況下在 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
}
有幾個 isRunning…
靜態方法,例如 isBrokerAndManagementRunning()
,它會驗證 Broker 是否已啟用管理外掛程式。
設定規則
有時您希望在沒有 Broker 時測試失敗,例如在夜間 CI 建置中。若要在執行階段停用規則,請將名為 RABBITMQ_SERVER_REQUIRED
的環境變數設定為 true
。
您可以使用 Setter 或環境變數覆寫 Broker 屬性,例如主機名稱
以下範例示範如何使用 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 連線(除非明確設定管理 URI)。
BrokerRunning
也提供了一個名為 setEnvironmentVariableOverrides
的 static
方法,可讓您傳入包含這些變數的地圖。它們會覆寫系統環境變數。如果您希望在多個測試套件中使用不同的測試組態,這可能會很有用。重要事項:必須在調用任何建立規則實例的 isRunning()
靜態方法之前呼叫此方法。變數值會套用至在此調用之後建立的所有實例。調用 clearEnvironmentVariableOverrides()
以重設規則以使用預設值(包括任何實際環境變數)。
在您的測試案例中,您可以在建立連線 Factory 時使用 brokerRunning
;getConnectionFactory()
會回傳規則的 RabbitMQ ConnectionFactory
。以下範例示範如何執行此操作
@Bean
public CachingConnectionFactory rabbitConnectionFactory() {
return new CachingConnectionFactory(brokerRunning.getConnectionFactory());
}
JUnit5 條件
2.0.2 版引入了對 JUnit5 的支援。
使用 @RabbitAvailable
註解
此類別層級註解類似於 JUnit4 @Rules
中討論的 BrokerRunning
@Rule
。它由 RabbitAvailableCondition
處理。
註解具有三個屬性
-
queues
:在每次測試之前宣告(和清除)並在所有測試完成時刪除的佇列陣列。 -
management
:如果您的測試也需要 Broker 上安裝的管理外掛程式,請將此項設定為true
。 -
purgeAfterEach
:(自 2.2 版起)當true
(預設值)時,queues
將在測試之間清除。
它用於檢查 Broker 是否可用,如果不可用,則跳過測試。如 設定規則 中所述,如果名為 RABBITMQ_SERVER_REQUIRED
的環境變數為 true
,則會導致在沒有 Broker 時測試快速失敗。您可以使用 設定規則 中討論的環境變數來設定條件。
此外,RabbitAvailableCondition
支援參數化測試建構函式和方法的引數解析。支援兩種引數類型
-
BrokerRunningSupport
:實例(在 2.2 之前,這是 JUnit 4BrokerRunning
實例) -
ConnectionFactory
:BrokerRunningSupport
實例的 RabbitMQ 連線 Factory
以下範例同時示範了兩者
@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()
的靜態方法取得條件的連線 Factory 的參考。
從 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
屬性中指定變數名稱。