测试支持
为异步应用编写集成测试,必然比测试简单应用更复杂。当 @RabbitListener 注解这样的抽象介入时,情况会更加复杂。问题在于:发送一条消息后,如何验证 listener 按预期收到了消息。
框架本身有许多单元测试和集成测试。有些使用 mock,有些则使用真实的 RabbitMQ broker 进行集成测试。你可以参考这些测试,为测试场景获取一些思路。
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(用于支持 @RabbitListener)相关的 bean。
@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 并倒数 latch 的 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 会捕获被测方法抛出的任何异常。使用 answer.getExceptions() 可获取对它们的引用。
与 @RabbitListenerTest 和 RabbitListenerTestHarness 结合使用时,使用 harness.getLambdaAnswerFor("listenerId", true, …) 为 listener 获取一个正确构造的 answer。
@RabbitListenerTest 与 RabbitListenerTestHarness
在某个 @Configuration 类上加注 @RabbitListenerTest,会使框架用一个名为 RabbitListenerTestHarness 的子类替换标准的 RabbitListenerAnnotationBeanPostProcessor(它还会通过 @EnableRabbit 启用 @RabbitListener 检测)。
RabbitListenerTestHarness 以两种方式增强 listener。首先,它把 listener 包装在一个 Mockito Spy 中,从而支持常规的 Mockito stub 和验证操作。它还可以向 listener 添加一个 Advice,从而访问参数、结果以及抛出的任何异常。你可以通过 @RabbitListenerTest 上的属性控制启用其中哪一种(或两者都启用)。后者用于访问关于调用的更底层数据。它还支持阻塞测试线程,直到异步 listener 被调用。
final 的 @RabbitListener 方法不能被 spy 或加 advice。此外,只有带 id 属性的 listener 才能被 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) {
...
}
}
@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());
}
}
把 harness 注入测试用例,以便访问 spy。
获取对 spy 的引用,以便验证它按预期被调用。由于这是
send和receive操作,无需挂起测试线程,因为它已经在RabbitTemplate中挂起等待回复。这里我们只使用发送操作,因此需要一个 latch 来等待容器线程上对 listener 的异步调用。我们使用 Answer<?> 实现之一来帮忙。IMPORTANT:由于 listener 被 spy 的方式,务必使用
harness.getLatchAnswerFor()为 spy 获取一个正确配置的 answer。配置 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;
}
}
@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
}
}
把 harness 注入测试用例,以便访问 spy。
使用
harness.getNextInvocationDataFor()获取调用数据——这里由于是请求/回复场景,无需等待,因为测试线程已在RabbitTemplate中挂起等待结果。然后我们可以验证参数和结果是否符合预期。
这一次我们需要等待一些时间来获取数据,因为这是容器线程上的异步操作,需要挂起测试线程。
当 listener 抛出异常时,可在调用数据的
throwable属性中获取。
在 harness 中使用自定义 Answer<?> 时,为使其正常工作,此类 answer 应子类化 ForwardsInvocation,并从 harness 获取实际 listener(而非 spy)(getDelegate("myListener")),然后调用 super.answer(invocation)。示例参见所提供的 Mockito Answer<?> 实现 源码。
使用 TestRabbitTemplate
提供 TestRabbitTemplate 用于在无需 broker 的情况下执行一些基本集成测试。当你把它作为 @Bean 添加到测试用例时,它会发现上下文中所有 listener 容器,无论它们是声明为 @Bean、<bean/> 还是使用 @RabbitListener 注解。它目前只支持按队列名称路由。template 从容器中提取消息监听器,并直接在测试线程上调用它。对于返回回复的 listener,支持请求-回复消息传递(sendAndReceive 方法)。
以下测试用例使用该 template:
@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:如果你的测试还要求 broker 上安装管理插件,则设为true。purgeAfterEach:(自 2.2 版本起)为true(默认)时,会在测试之间清空queues。
它用于检查 broker 是否可用,若不可用则跳过测试。
有时你希望在没有 broker 时让测试失败,例如夜间 CI 构建。要在运行时禁用 BrokerRunningSupport,把名为 RABBITMQ_SERVER_REQUIRED 的环境变量设为 true。
你可以用 setter 或环境变量覆盖 broker 属性,例如主机名。
以下示例展示如何用 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/](http://localhost:15672/api/))。
更改主机名会同时影响 amqp 和 management REST API 连接(除非显式设置了 admin uri)。
BrokerRunningSupport 还提供一个名为 setEnvironmentVariableOverrides 的 static 方法,允许你传入包含这些变量的 map。它们会覆盖系统环境变量。如果你希望在多个测试套件中为测试使用不同配置,这可能很有用。IMPORTANT:必须在调用任何会创建规则实例的 isRunning() static 方法之前调用此方法。变量值会应用于本次调用之后创建的所有实例。调用 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() 的 static 方法获取对该条件连接工厂的引用。
以下测试来自框架,演示其用法:
@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 属性中指定变量名。