为异步应用程序编写集成必然比测试更简单的应用程序更复杂。
当诸如注释之类的抽象概念出现时,这变得更加复杂。
问题是如何验证在发送消息后,侦听器是否按预期接收了消息。@RabbitListener
该框架本身有许多单元和集成测试。 有些使用模拟测试,而另一些则使用实时 RabbitMQ 代理的集成测试。 您可以查阅这些测试,以获取有关测试方案的一些想法。
Spring AMQP 版本 1.6 引入了 jar,它为测试其中一些更复杂的方案提供了支持。
预计这个项目会随着时间的推移而扩展,但我们需要社区反馈,以便对帮助测试所需的功能提出建议。
请使用 JIRA 或 GitHub Issues 提供此类反馈。spring-rabbit-test
@SpringRabbitTest
使用此注解将基础结构 Bean 添加到 Spring 测试中。
例如,使用时不需要这样做,因为 Spring Boot 的自动配置将添加 bean。ApplicationContext
@SpringBootTest
已注册的 Bean 包括:
-
CachingConnectionFactory
(autoConnectionFactory
).如果存在,则使用其连接工厂。@RabbitEnabled
-
RabbitTemplate
(autoRabbitTemplate
) -
RabbitAdmin
(autoRabbitAdmin
) -
RabbitListenerContainerFactory
(autoContainerFactory
)
此外,还添加了与 (支持 ) 关联的 bean。@EnableRabbit
@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();
}
}
以下类测试 POJO:Thing
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
用注释其中一个类会导致框架替换
带有一个名为 (它还允许通过 进行检测) 的子类 standard 。@Configuration
@RabbitListenerTest
RabbitListenerAnnotationBeanPostProcessor
RabbitListenerTestHarness
@RabbitListener
@EnableRabbit
这以两种方式增强了听众。
首先,它将侦听器包装在 中,启用正常的存根和验证操作。
它还可以向侦听器添加一个,从而允许访问参数、结果和引发的任何异常。
您可以使用 上的属性控制其中的哪些(或两者)启用。
后者用于访问有关调用的较低级别数据。
它还支持阻止测试线程,直到调用异步侦听器。RabbitListenerTestHarness
Mockito Spy
Mockito
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 | 获取对间谍的引用,以便我们可以验证它是否按预期调用。
由于这是发送和接收操作,因此无需挂起测试线程,因为它已经
暂停在等待回复。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 |
当将自定义 s 与线束一起使用时,为了正常操作,此类答案应子类化并从线束 () 获取实际侦听器(而不是间谍)并调用 .
有关示例,请参阅提供的 Mockito Answer<?> 实现源代码。Answer<?> ForwardsInvocation getDelegate("myListener") super.answer(invocation) |
final @RabbitListener 方法不能被窥探或建议。
此外,只有具有属性的侦听器才能被监视或建议。id |
1 | 将安全带注入测试用例,以便我们可以访问间谍。 |
2 | 获取对间谍的引用,以便我们可以验证它是否按预期调用。
由于这是发送和接收操作,因此无需挂起测试线程,因为它已经
暂停在等待回复。RabbitTemplate |
3 | 在本例中,我们只使用发送操作,因此我们需要一个闩锁来等待对侦听器的异步调用
在容器线程上。
我们使用 Answer<?> 实现之一来帮助实现这一点。
重要提示:由于侦听器被监视的方式,使用来为间谍获取正确配置的答案非常重要。harness.getLatchAnswerFor() |
4 | 配置侦测以调用 .Answer |
1 | 将安全带注入测试用例,以便我们可以访问间谍。 |
2 | 用于检索调用数据 - 在本例中,因为它是请求/应答
场景:无需等待任何时间,因为测试线程在等待中挂起
为了结果。harness.getNextInvocationDataFor() RabbitTemplate |
3 | 然后,我们可以验证参数和结果是否符合预期。 |
4 | 这次我们需要一些时间来等待数据,因为它是容器线程上的异步操作,我们需要 挂起测试线程。 |
5 | 当侦听器引发异常时,它在调用数据的属性中可用。throwable |
当将自定义 s 与线束一起使用时,为了正常操作,此类答案应子类化并从线束 () 获取实际侦听器(而不是间谍)并调用 .
有关示例,请参阅提供的 Mockito Answer<?> 实现源代码。Answer<?> ForwardsInvocation getDelegate("myListener") super.answer(invocation) |
用TestRabbitTemplate
提供用于执行一些基本集成测试,而无需代理。
当您在测试用例中将其添加为 a 时,它会发现上下文中的所有侦听器容器,无论是声明为 还是使用注释。
它目前仅支持按队列名称进行路由。
模板从容器中提取消息侦听器,并直接在测试线程上调用它。
返回回复的侦听器支持请求-回复消息传递(方法)。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 及更高版本提供了一个名为 .
此 jar 包含几个实用程序实例,用于运行 JUnit4 测试。
请参阅 JUnit5 测试的 JUnit5 条件。spring-rabbit-junit
@Rule
用BrokerRunning
BrokerRunning
提供了一种机制,以便在代理未运行时让测试成功(默认为 on )。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 和管理 REST API)。localhost:5672
localhost:15672/api/
更改主机名会影响 API 和 REST API 连接(除非显式设置了管理 URI)。amqp
management
BrokerRunning
还提供了一个名为 that 的方法,该方法允许您传入包含这些变量的映射。
它们覆盖系统环境变量。
如果您希望在多个测试套件中对测试使用不同的配置,这可能很有用。
重要说明:在调用创建规则实例的任何静态方法之前,必须调用该方法。
变量值将应用于此调用后创建的所有实例。
调用以重置规则以使用默认值(包括任何实际环境变量)。static
setEnvironmentVariableOverrides
isRunning()
clearEnvironmentVariableOverrides()
在测试用例中,可以在创建连接工厂时使用 ; 返回规则的 RabbitMQ 。
以下示例演示如何执行此操作:brokerRunning
getConnectionFactory()
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
它用于检查代理是否可用,如果没有,则跳过测试。
如配置规则中所述,如果没有代理,名为 , 的环境变量会导致测试快速失败。
您可以使用环境变量配置条件,如配置规则中所述。RABBITMQ_SERVER_REQUIRED
true
此外,还支持参数化测试构造函数和方法的参数解析。
支持两种参数类型:RabbitAvailableCondition
-
BrokerRunningSupport
:实例(在 2.2 之前,这是一个 JUnit 4 实例)BrokerRunning
-
ConnectionFactory
:实例的 RabbitMQ 连接工厂BrokerRunningSupport
以下示例显示了两者:
@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 开始,返回一个对象;以前,返回了 JUnit 4 实例。
新类具有与 相同的 API。getBrokerRunning() BrokerRunningSupport BrokerRunnning BrokerRunning |
以下测试来自框架,并演示了用法:
@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
@Rule
true
@RabbitAvailable(queues = SimpleMessageListenerContainerLongTests.QUEUE)
@LongRunning
public class SimpleMessageListenerContainerLongTests {
public static final String QUEUE = "SimpleMessageListenerContainerLongTests.queue";
...
}
默认情况下,变量为 ,但您可以在注释的属性中指定变量名称。RUN_LONG_INTEGRATION_TESTS
value
从版本 2.2 开始,返回一个对象;以前,返回了 JUnit 4 实例。
新类具有与 相同的 API。getBrokerRunning() BrokerRunningSupport BrokerRunnning BrokerRunning |