测试支持

为异步应用程序编写集成必然比测试更简单的应用程序更复杂。 当 annotation 等抽象出现时,情况会变得更加复杂。 问题是如何验证在发送消息后,侦听器是否按预期收到了消息。@RabbitListenerspring-doc.cn

框架本身有许多单元测试和集成测试。 一些使用模拟,而另一些则使用实时 RabbitMQ 代理的集成测试。 您可以查阅这些测试,了解测试方案的一些想法。spring-doc.cn

Spring AMQP 版本 1.6 引入了 jar,它为测试其中一些更复杂的场景提供了支持。 预计该项目将随着时间的推移而扩展,但我们需要社区反馈,以便为帮助测试所需的功能提出建议。 请使用 JIRAGitHub Issues 提供此类反馈。spring-rabbit-testspring-doc.cn

@SpringRabbitTest

使用此注释将基础结构 bean 添加到 Spring test 中。 使用时这不是必需的,例如,因为 Spring Boot 的自动配置将添加 bean。ApplicationContext@SpringBootTestspring-doc.cn

已注册的 Bean 包括:spring-doc.cn

  • CachingConnectionFactory (autoConnectionFactory).如果存在,则使用其 connection factory。@RabbitEnabledspring-doc.cn

  • RabbitTemplate (autoRabbitTemplate)spring-doc.cn

  • RabbitAdmin (autoRabbitAdmin)spring-doc.cn

  • RabbitListenerContainerFactory (autoContainerFactory)spring-doc.cn

此外,还添加了与 (to support ) 关联的 bean。@EnableRabbit@RabbitListenerspring-doc.cn

Junit5 示例
@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)spring-doc.cn

Mockito 实现Answer<?>

目前有两种 implementation 可以帮助进行测试。Answer<?>spring-doc.cn

第一个 , , 提供一个 返回 latch 并倒计时。 以下示例演示如何使用:LatchCountDownAndCallRealMethodAnswerAnswer<Void>nullLatchCountDownAndCallRealMethodAnswerspring-doc.cn

LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("myListener", 2);
doAnswer(answer)
    .when(listener).foo(anyString(), anyString());

...

assertThat(answer.await(10)).isTrue();

第二个选项提供了一种机制,可以选择性地调用真实方法,并提供一个机会 以根据 和 结果(如果有)返回自定义结果。LambdaAnswer<T>InvocationOnMockspring-doc.cn

考虑以下 POJO:spring-doc.cn

public class Thing {

    public String thing(String thing) {
        return thing.toUpperCase();
    }

}

以下类测试 POJO:Thingspring-doc.cn

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()spring-doc.cn

当与@RabbitListenerTestRabbitListenerTestHarness结合使用时,用于为侦听器获取正确构建的答案。harness.getLambdaAnswerFor("listenerId", true, …​)spring-doc.cn

@RabbitListenerTestRabbitListenerTestHarness

为其中一个类添加 注解会导致框架将 standard 替换为一个名为 (它还允许通过 ) 进行检测。@Configuration@RabbitListenerTestRabbitListenerAnnotationBeanPostProcessorRabbitListenerTestHarness@RabbitListener@EnableRabbitspring-doc.cn

这会以两种方式增强侦听器。 首先,它将侦听器包装在 中,从而启用正常的存根和验证操作。 它还可以向侦听器添加 an,从而允许访问参数、结果和引发的任何异常。 您可以通过 上的属性控制启用其中的哪些(或两者)。 后者用于访问有关调用的较低级别数据。 它还支持阻塞测试线程,直到异步侦听器被调用。RabbitListenerTestHarnessMockito SpyMockitoAdvice@RabbitListenerTestspring-doc.cn

final @RabbitListener方法不能被窥探或建议。 此外,只能侦测或建议具有属性的侦听器。id

请看看一些例子。spring-doc.cn

以下示例使用 spy:spring-doc.cn

@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 在这种情况下,我们只使用 send 操作,因此我们需要一个 latch 来等待对侦听器的异步调用 在容器线程上。 我们使用 Answer<?> 实现之一来帮助解决这个问题。 重要说明:由于侦听器的侦测方式,请务必使用 来获取 spy 的正确配置答案。harness.getLatchAnswerFor()
4 配置 spy 以调用 .Answer

以下示例使用 capture 建议:spring-doc.cn

@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
当将自定义 s 与 harness 一起使用时,为了正常运行,此类答案应子类化并从 harness () 获取实际的侦听器(不是间谍)并调用。 有关示例,请参阅提供的 Mockito Answer<?> Implementations 源代码。Answer<?>ForwardsInvocationgetDelegate("myListener")super.answer(invocation)

TestRabbitTemplate

提供 是为了执行一些基本的集成测试,而无需代理。 当您将其添加为测试用例中的时,它会发现上下文中的所有侦听器容器,无论是声明为 OR 还是使用注释。 它目前仅支持按队列名称进行路由。 该模板从容器中提取消息侦听器,并直接在测试线程上调用它。 返回回复的侦听器支持请求-回复消息传递 ( methods)。TestRabbitTemplate@Bean@Bean<bean/>@RabbitListenersendAndReceivespring-doc.cn

以下测试用例使用模板:spring-doc.cn

@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。 此 jar 包含几个实用程序实例,用于运行 JUnit4 测试。 请参阅 JUnit5 条件 以了解 JUnit5 测试。spring-rabbit-junit@Rulespring-doc.cn

BrokerRunning

BrokerRunning提供了一种机制,允许测试在代理未运行时成功(默认情况下为 on , )。localhostspring-doc.cn

它还具有用于初始化和清空队列以及删除队列和交换的实用方法。spring-doc.cn

以下示例显示了其用法:spring-doc.cn

@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
}

有几种静态方法,例如 ,用于验证 broker 是否启用了管理插件。isRunning…​isBrokerAndManagementRunning()spring-doc.cn

配置规则

有时,如果没有代理,例如夜间 CI 构建,您希望测试失败。 要在运行时禁用规则,请将名为 的环境变量设置为 .RABBITMQ_SERVER_REQUIREDtruespring-doc.cn

您可以使用 setter 或环境变量覆盖代理属性,例如 hostname:spring-doc.cn

下面的示例演示如何使用 setter 覆盖属性:spring-doc.cn

@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
}

您还可以通过设置以下环境变量来覆盖属性:spring-doc.cn

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:5672localhost:15672/api/spring-doc.cn

更改主机名会影响 和 REST API 连接(除非显式设置了管理员 uri)。amqpmanagementspring-doc.cn

BrokerRunning还提供了一个名为 Method 的方法,用于传入包含这些变量的 Map。 它们会覆盖系统环境变量。 如果您希望对多个测试套件中的测试使用不同的配置,这可能很有用。 重要说明:在调用创建规则实例的任何静态方法之前,必须调用该方法。 变量值将应用于此调用后创建的所有实例。 调用 以重置规则以使用默认值(包括任何实际的环境变量)。staticsetEnvironmentVariableOverridesisRunning()clearEnvironmentVariableOverrides()spring-doc.cn

在测试用例中,您可以在创建连接工厂时使用 ; 返回规则的 RabbitMQ 。 以下示例显示了如何执行此操作:brokerRunninggetConnectionFactory()ConnectionFactoryspring-doc.cn

@Bean
public CachingConnectionFactory rabbitConnectionFactory() {
    return new CachingConnectionFactory(brokerRunning.getConnectionFactory());
}

LongRunningIntegrationTest

LongRunningIntegrationTest是禁用长时间运行的测试的规则。 您可能希望在开发人员系统上使用它,但请确保在夜间 CI 构建等上禁用该规则。spring-doc.cn

以下示例显示了其用法:spring-doc.cn

@Rule
public LongRunningIntegrationTest longTests = new LongRunningIntegrationTest();

要在运行时禁用规则,请将名为 的环境变量设置为 .RUN_LONG_INTEGRATION_TESTStruespring-doc.cn

JUnit5 条件

版本 2.0.2 引入了对 JUnit5 的支持。spring-doc.cn

使用注释@RabbitAvailable

此类级注释类似于 JUnit4 @Rules中讨论的注释。 它由 处理。BrokerRunning@RuleRabbitAvailableConditionspring-doc.cn

该批注具有三个属性:spring-doc.cn

  • queues:在每次测试之前声明(和清除)并在所有测试完成时删除的队列数组。spring-doc.cn

  • management:如果您的测试还需要在代理上安装管理插件,请将其设置为。truespring-doc.cn

  • purgeAfterEach: (自版本 2.2 起) when (default) 时,将在测试之间清除。truequeuesspring-doc.cn

它用于检查 broker 是否可用,如果没有,则跳过测试。 如 配置规则中所述,如果没有代理,则名为 if 的环境变量会导致测试快速失败。 您可以使用环境变量配置条件,如 配置规则中所述。RABBITMQ_SERVER_REQUIREDtruespring-doc.cn

此外,还支持参数化测试构造函数和方法的参数解析。 支持两种参数类型:RabbitAvailableConditionspring-doc.cn

  • BrokerRunningSupport:实例(在 2.2 之前,这是一个 JUnit 4 实例)BrokerRunningspring-doc.cn

  • ConnectionFactory:实例的 RabbitMQ 连接工厂BrokerRunningSupportspring-doc.cn

以下示例显示了两者:spring-doc.cn

@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();
    }

}

前面的测试在框架本身中,验证参数注入以及条件是否正确创建了队列。spring-doc.cn

实际的用户测试可能如下所示:spring-doc.cn

@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()spring-doc.cn

从版本 2.2 开始,返回一个对象;以前,返回 JUnit 4 实例。 新类具有与 . 相同的 API 。getBrokerRunning()BrokerRunningSupportBrokerRunnningBrokerRunning

以下测试来自框架,演示了用法:spring-doc.cn

@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@Ruletruespring-doc.cn

@RabbitAvailable(queues = SimpleMessageListenerContainerLongTests.QUEUE)
@LongRunning
public class SimpleMessageListenerContainerLongTests {

    public static final String QUEUE = "SimpleMessageListenerContainerLongTests.queue";

...

}

默认情况下,变量为 ,但您可以在注释的属性中指定变量名称。RUN_LONG_INTEGRATION_TESTSvaluespring-doc.cn