| 
         对于最新的稳定版本,请使用 Spring AMQP 3.2.6!  | 
    
测试支持
为异步应用程序编写集成必然比测试更简单的应用程序更复杂。
当抽象(例如@RabbitListener注释出现在画面中。
问题是如何验证在发送消息后,侦听器是否按预期收到消息。
框架本身有许多单元测试和集成测试。 有些使用模拟,而另一些则使用与实时 RabbitMQ 代理的集成测试。 您可以查阅这些测试,以获取测试方案的一些想法。
Spring AMQP 1.6 版引入了spring-rabbit-testjar,它支持测试其中一些更复杂的场景。
预计该项目将随着时间的推移而扩展,但我们需要社区反馈,以便为帮助测试所需的功能提出建议。
请使用 JIRA 或 GitHub Issues 提供此类反馈。
@SpringRabbitTest
使用此注释将基础架构 Bean 添加到 Spring 测试中ApplicationContext.
例如,在使用@SpringBootTest因为 Spring Boot 的自动配置将添加 bean。
注册的 Bean 是:
- 
CachingConnectionFactory(autoConnectionFactory).如果@RabbitEnabled存在,则使用其连接工厂。 - 
RabbitTemplate(autoRabbitTemplate) - 
RabbitAdmin(autoRabbitAdmin) - 
RabbitListenerContainerFactory(autoContainerFactory) 
此外,与@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).
莫基托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();
    }
}
以下类测试ThingPOJO:
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导致框架将
标准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 | 配置 spy 以调用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调用数据的属性。 | 
使用自定义时Answer<?>s 与线束,为了正常运行,此类答案应 subclassForwardsInvocation并从线束 (getDelegate("myListener")) 并调用super.answer(invocation). 请参阅提供的莫基托Answer<?>实现示例的源代码。 | 
用TestRabbitTemplate
这TestRabbitTemplate用于执行一些基本的集成测试,而无需代理。当您将其添加为@Bean在您的测试用例中,它会发现上下文中的所有侦听器容器,无论是否声明为@Bean或<bean/>或使用@RabbitListener注解。 它目前仅支持按队列名称进行路由。该模板从容器中提取消息侦听器,并直接在测试线程上调用它。请求-回复消息传递 (sendAndReceivemethods) 支持返回回复的侦听器。
以下测试用例使用模板:
@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. 这个罐子包含几个实用程序@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…static 方法,例如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";
这些环境变量会覆盖默认设置 (localhost:5672对于 AMQP 和localhost:15672/api/用于管理 REST API)。
更改主机名会影响amqp和managementREST API 连接(除非显式设置了管理 uri)。
BrokerRunning还提供了一个static调用的方法setEnvironmentVariableOverrides这使您可以传入包含这些变量的映射。
它们覆盖系统环境变量。
如果您希望对多个测试套件中的测试使用不同的配置,这可能很有用。
重要提示:在调用任何isRunning()创建规则实例的静态方法。
变量值将应用于在此调用之后创建的所有实例。
调用clearEnvironmentVariableOverrides()重置规则以使用默认值(包括任何实际环境变量)。
在测试用例中,您可以使用brokerRunning创建连接工厂时;getConnectionFactory()返回规则的 RabbitMQConnectionFactory.
以下示例显示了如何执行此作:
@Bean
public CachingConnectionFactory rabbitConnectionFactory() {
    return new CachingConnectionFactory(brokerRunning.getConnectionFactory());
}
JUnit5 条件
版本 2.0.2 引入了对 JUnit5 的支持。
使用@RabbitAvailable注解
此类级注解类似于BrokerRunning @Rule讨论于JUnit4@Rules.
它由RabbitAvailableCondition.
注释具有三个属性:
- 
queues:在每次测试之前声明(和清除)并在所有测试完成后删除的队列数组。 - 
management:将此设置为true如果您的测试还需要在代理上安装管理插件。 - 
purgeAfterEach:(从 2.2 版开始)当true(默认)、queues将在测试之间清除。 
它用于检查代理是否可用,如果没有,则跳过测试。
如配置规则中所述,名为RABBITMQ_SERVER_REQUIRED如果true,如果没有代理,则会导致测试快速失败。
您可以使用环境变量来配置条件,如配置规则中所述。
此外,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 4BrokerRunnning实例被返回。
新类具有与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注解
类似于LongRunningIntegrationTestJUnit4@Rule,除非将环境变量(或系统属性)设置为true.
以下示例演示如何使用它:
@RabbitAvailable(queues = SimpleMessageListenerContainerLongTests.QUEUE)
@LongRunning
public class SimpleMessageListenerContainerLongTests {
    public static final String QUEUE = "SimpleMessageListenerContainerLongTests.queue";
...
}
默认情况下,变量为RUN_LONG_INTEGRATION_TESTS,但您可以在注释的value属性。