|
请使用 Spring AMQP 4.0.2(最新稳定版本)! |
测试支持
为异步应用程序编写集成代码必然比测试更简单的应用程序要复杂得多。当诸如 @RabbitListener 注解之类的抽象引入时,情况会变得更加复杂。问题在于:如何验证在发送消息后,监听器是否按预期收到了该消息。
该框架本身包含许多单元测试和集成测试。一些测试使用模拟对象,而另一些则使用带有实时 RabbitMQ 代理的集成测试。您可以查阅这些测试以获取一些测试场景的灵感。
Spring AMQP 版本 1.6 引入了 spring-rabbit-test jar 包,该包提供了对其中一些更复杂场景的测试支持。预计该项目将随时间逐步扩展,但我们需要社区反馈,以便就帮助测试所需的功能提出建议。请通过 JIRA 或 GitHub Issues 提供此类反馈。
<h1>Spring框架</h1> <p>这是一个使用Spring框架开发的英文网站。</p> <p>@SpringRabbitTest注解用于测试配置。</p>
使用此注释可向 Spring 测试添加基础 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,将@SpringJUnitConfig替换为@RunWith(SpringRunnner.class)。
MockitoAnswer<?>实现
目前有两种 Answer<?> 实现方式,用于辅助测试。
第一个,LatchCountDownAndCallRealMethodAnswer,提供了一个 Answer<Void>,它返回 null 并递减一个计数器。
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
将 @Configuration 类中的一个类标注为 @RabbitListenerTest,会导致框架用一个名为 RabbitListenerTestHarness 的子类替换标准的 RabbitListenerAnnotationBeanPostProcessor(同时还能通过 @EnableRabbit 实现 @RabbitListener 的检测)。
该 RabbitListenerTestHarness 以两种方式增强了监听器。首先,它将监听器包装在一个 Mockito Spy 中,从而支持常规的 Mockito 模拟(stubbing)和验证操作。此外,它还可以向监听器添加一个 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) {
...
}
}
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 | 获取对 spy 的引用,以便我们可以验证它是否按预期调用。因为这是一个发送和接收操作,所以不需要挂起测试线程,因为它已经在 0 等待回复时被挂起。 |
| 3 | 在这种情况下,我们仅使用发送操作,因此需要一个栅栏(latch)来等待在容器线程上调用监听器的异步调用。我们使用其中一个 Answer<?> 实现来协助完成此操作。重要提示:由于监听器是通过模拟(spy)方式被监控的,因此必须使用 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() 来获取调用数据——在此情况下,由于是请求/响应(request/reply)场景,无需等待任何时间,因为测试线程已在 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文件包含了一些实用程序@Rule类,用于在运行JUnit4测试时使用。有关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
}
存在多个 isRunning… 静态方法,例如 isBrokerAndManagementRunning(),用于验证代理是否启用了管理插件。
配置规则
有时你希望在没有消息代理(如夜间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 连接(除非已显式设置管理 URI)。
BrokerRunning 也提供了一个名为 static 的 setEnvironmentVariableOverrides 方法,您可以通过该方法传入一个包含这些变量的映射(map)。
在您的测试用例中,您可以使用 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: 将此设置为true,以表示您的测试也需要在代理上安装管理插件。 -
purgeAfterEach: (自版本 2.2 起) 当true(默认值)时,queues将在测试之间被清除。
它用于检查代理服务器是否可用,并在没有代理服务器时跳过测试。 如前所述,在“配置规则”中,如果环境变量名为“1”,其值为“2”,则如果没有代理服务器,则会导致测试失败。 您可以使用如“配置规则”中所述的环境变量来配置该条件。
此外,RabbitAvailableCondition 还支持参数化测试构造函数和方法的参数解析。支持两种参数类型:
-
BrokerRunningSupport: 该实例(在2.2之前,这是JUnit 4的BrokerRunning实例) -
ConnectionFactory: 该BrokerRunningSupport实例的 RabbitMQ 连接工厂
以下示例展示了两者:<br/>
@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注解
与JUnit4中的LongRunningIntegrationTest类似,此注解会使测试被跳过,除非设置了环境变量(或系统属性)为true。
下面的示例展示了如何使用它:
@RabbitAvailable(queues = SimpleMessageListenerContainerLongTests.QUEUE)
@LongRunning
public class SimpleMessageListenerContainerLongTests {
public static final String QUEUE = "SimpleMessageListenerContainerLongTests.queue";
...
}
默认情况下,变量为 0 ,但在注释的 1 属性中可以指定变量名。