Testing Support
Spring AMQP 框架提供了单元和集成测试,可以使用模拟或实时 RabbitMQ 代理。可以查看这些测试用例以收集一些测试方案的思路。
编写异步应用程序的集成必须比测试更简单的应用程序复杂。当诸如 @RabbitListener
注解之类的抽象概念进入图片时,这变得更加复杂。问题是如何验证在发送消息后,侦听器是否按预期收到了消息。
框架本身有许多单元和集成测试。有些使用模拟,而另一些使用具有实时 RabbitMQ 代理的集成测试。你可以查看这些测试,了解一些测试方案的想法。
Spring AMQP 版本 1.6 引入了 `spring-rabbit-test`jar,它提供了对其中某些更复杂场景的测试支持。预计此项目会随着时间的推移而扩展,但我们需要社区反馈以提出有关有助于测试所需功能的建议。请使用 JIRA或 GitHub Issues提供此类反馈。
@SpringRabbitTest
使用此注释向 Spring 测试 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 {
...
}
}
对于 JUnit4,用 @RunWith(SpringRunnner.class)
替换 @SpringJUnitConfig
。
Mockito Answer<?>
Implementations
目前有两种 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.getExceptions()
获取它们的引用。
当与 xref:testing.adoc#test-harness[@RabbitListenerTest
和 RabbitListenerTestHarness
结合使用时,请使用 harness.getLambdaAnswerFor("listenerId", true, …)
为侦听器获取正确构建的答案。
@RabbitListenerTest
and RabbitListenerTestHarness
使用 @RabbitListenerTest
注释某个 @Configuration
类,会导致框架用一个称为 RabbitListenerTestHarness
的子类来替换标准的 RabbitListenerAnnotationBeanPostProcessor
(它还通过 @EnableRabbit
启用 @RabbitListener
检测)。
RabbitListenerTestHarness
以两种方式增强侦听器。首先,它将侦听器包装在 Mockito Spy
中,启用正常的 Mockito
存根和验证操作。它还可以在侦听器中添加 Advice
,来启用对参数、结果和任何抛出的异常的访问。您可以通过 @RabbitListenerTest
上的属性来控制启用哪些(或两者同时启用)。后者用于访问关于调用的较低级别的数据。它还支持阻塞测试线程,直至调用异步侦听器。
和方法无法被监视或建议。此外,只有带属性的侦听器可以被监视或建议。
考虑一些示例。
以下示例使用间谍:
@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 | 在这种情况下,我们只使用一个发送操作,所以我们需要一个闩锁等待容器线程上的侦听器的异步调用。我们使用其中一个 Answer<?> 实现来提供帮助。重要信息:由于侦听器被监视的方式,使用 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 属性中获取。 |
在将自定义与测试工具配合使用时,为了正常运行,此类解答应当对进行子类化,并从测试工具获取实际侦听器(不是侦听器间谍),然后调用。有关示例,请参阅提供的实现源代码。
Using 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 Conditions。
Using 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
}
有一些 isRunning…
静态方法,例如 isBrokerAndManagementRunning()
,用于验证代理是否启用了管理插件。
Configuring the Rule
有时,当没有代理(例如每晚的 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 为 http://localhost:15672/api/
)。
更改主机名会影响 amqp
和 management
REST API 连接(除非明确设置管理 uri)。
BrokerRunning
还提供了一个名为 setEnvironmentVariableOverrides
的 static
方法,该方法允许您传入包含这些变量的映射。它们会覆盖系统环境变量。如果您希望在多个测试套件中对测试使用不同的配置,这可能很有用。重要信息:该方法必须在调用创建规则实例的任何 isRunning()
静态方法之前被调用。变量值应用于此调用之后创建的所有实例。调用 clearEnvironmentVariableOverrides()
可将规则重置为使用默认值(包括任何实际环境变量)。
在你的测试用例中,你可以在创建连接工厂时使用 brokerRunning
; getConnectionFactory()
返回规则的 RabbitMQ ConnectionFactory
. 以下示例展示了如何操作:
@Bean
public CachingConnectionFactory rabbitConnectionFactory() {
return new CachingConnectionFactory(brokerRunning.getConnectionFactory());
}
JUnit5 Conditions
版本 2.0.2 引入了对 JUnit5 的支持。
Using the @RabbitAvailable
Annotation
这个类级别的注释与 junit-rules
中讨论的 BrokerRunning
@Rule
类似。它由 RabbitAvailableCondition
处理。
该注释有三个属性:
-
queues
:在每个测试之前声明(并清除)的队列数组,并在所有测试完成后删除。 -
management
:如果测试还要求经纪人上安装管理插件,请将此设置为true
。 -
purgeAfterEach
:当true
(默认)时(2.2 版起),queues
将在测试之间清除。
它用于检查代理是否可用,如果没有,则跳过测试。正如 Configuring the Rule 中所述,如果为 true
,名为 RABBITMQ_SERVER_REQUIRED
的环境变量会导致在没有代理时快速使测试失败。如 Configuring the Rule 中所述,可以使用环境变量配置条件。
此外,RabbitAvailableCondition
支持对参数化测试构造函数和方法的参数解析。支持两种参数类型:
-
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;
}
}
}
Using the @LongRunning
Annotation
与 LongRunningIntegrationTest
JUnit4 @Rule
类似,此注释会导致测试被跳过(直到环境变量(或系统属性)被设置为 true
)。以下示例展示了如何使用它:
@RabbitAvailable(queues = SimpleMessageListenerContainerLongTests.QUEUE)
@LongRunning
public class SimpleMessageListenerContainerLongTests {
public static final String QUEUE = "SimpleMessageListenerContainerLongTests.queue";
...
}
默认情况下,变量是 RUN_LONG_INTEGRATION_TESTS
,但你可以在注释的 value
属性中指定变量名。