测试支持
为异步应用编写集成测试必然比测试简单应用更复杂。当引入诸如 @RabbitListener 注解之类的抽象时,复杂性会进一步增加。问题在于如何验证在发送消息后,监听器按预期接收到了消息。
框架本身有许多单元测试和集成测试。有些使用模拟对象,另一些则使用真实的 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`,则使用其连接工厂。 -
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 {
...
}
}
使用 JUnit 4 时,将 `@SpringJUnitConfig` 替换为 `@RunWith(SpringRunnner.class)`。
Mockito Answer>
实现
目前提供了两种 Answer>
实现来协助测试。
第一种是 LatchCountDownAndCallRealMethodAnswer
,它提供了 Answer
的实现,返回 `null` 并递减一个计数闩 (latch)。以下示例展示了如何使用 LatchCountDownAndCallRealMethodAnswer
LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("myListener", 2);
doAnswer(answer)
.when(listener).foo(anyString(), anyString());
...
assertThat(answer.await(10)).isTrue();
第二种是 LambdaAnswer
,它提供了一种机制,可以选择性地调用真实方法,并有机会根据 `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
类,会使框架用一个名为 RabbitListenerTestHarness
的子类替换标准的 RabbitListenerAnnotationBeanPostProcessor
(它也通过 @EnableRabbit
启用了 @RabbitListener
检测)。
RabbitListenerTestHarness
以两种方式增强了监听器。首先,它将监听器包装在一个 Mockito Spy
中,从而实现正常的 Mockito
存根和验证操作。它还可以向监听器添加一个 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<?> 时,为了正常运行,这些 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;
}
}
}
JUnit 4 @Rules
Spring AMQP 1.7 及更高版本提供了一个额外的 jar 包,名为 spring-rabbit-junit
。该 jar 包含一些实用工具 @Rule
实例,用于运行 JUnit 4 测试。有关 JUnit 5 测试,请参阅 JUnit 5 Conditions。
使用 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 是否启用了管理插件。
配置 Rule
有时您希望在没有 broker 时测试失败,例如在夜间 CI 构建中。要在运行时禁用该 rule,请将名为 RABBITMQ_SERVER_REQUIRED
的环境变量设置为 true
。
您可以使用 setter 方法或环境变量覆盖 broker 属性,例如 hostname
以下示例展示了如何使用 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
方法,允许您传入一个包含这些变量的 map。它们会覆盖系统环境变量。如果您希望在多个测试套件中使用不同的测试配置,这可能会很有用。重要提示:在调用任何创建 rule 实例的 isRunning()
静态方法之前,必须调用此方法。变量值将应用于此调用之后创建的所有实例。调用 clearEnvironmentVariableOverrides()
可以将 rule 重置为使用默认设置(包括任何实际的环境变量)。
在您的测试用例中,创建连接工厂时可以使用 brokerRunning
;getConnectionFactory()
返回该 rule 的 RabbitMQ ConnectionFactory
。以下示例展示了如何操作
@Bean
public CachingConnectionFactory rabbitConnectionFactory() {
return new CachingConnectionFactory(brokerRunning.getConnectionFactory());
}
JUnit 5 Conditions
2.0.2 版本引入了对 JUnit 5 的支持。
使用 @RabbitAvailable
注解
这个类级别注解类似于 JUnit 4 @Rules
中讨论的 BrokerRunning
@Rule
。它由 RabbitAvailableCondition
处理。
该注解有三个属性
-
queues
:一个队列数组,在每个测试之前声明(并清空),并在所有测试完成后删除。 -
management
:如果您的测试还需要在 broker 上安装管理插件,则将其设置为true
。 -
purgeAfterEach
:(自 2.2 版本起)当为true
(默认值)时,queues
会在测试之间被清空。
它用于检查 broker 是否可用,如果不可用则跳过测试。如 配置 Rule 中所述,如果名为 RABBITMQ_SERVER_REQUIRED
的环境变量为 true
,则在没有 broker 时会立即导致测试失败。您可以使用 配置 Rule 中讨论的环境变量来配置该 condition。
此外,RabbitAvailableCondition
支持对参数化测试构造函数和方法进行参数解析。支持两种参数类型
-
BrokerRunningSupport
:实例(在 2.2 版本之前,这是一个 JUnit 4 的BrokerRunning
实例) -
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();
}
}
前面的测试位于框架本身中,用于验证参数注入以及 condition 正确创建了队列。
一个实际的用户测试可能如下所示
@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()
的静态方法获取 condition 的连接工厂的引用。
从 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
JUnit 4 @Rule
,此注解会导致测试被跳过,除非某个环境变量(或系统属性)设置为 true
。以下示例展示了如何使用它
@RabbitAvailable(queues = SimpleMessageListenerContainerLongTests.QUEUE)
@LongRunning
public class SimpleMessageListenerContainerLongTests {
public static final String QUEUE = "SimpleMessageListenerContainerLongTests.queue";
...
}
默认情况下,变量名为 RUN_LONG_INTEGRATION_TESTS
,但您可以在注解的 value
属性中指定变量名。