测试支持

为异步应用程序编写集成测试必然比测试更简单的应用程序复杂。当诸如 @RabbitListener 注解之类的抽象引入时,这会变得更加复杂。问题在于如何验证在发送消息后,监听器按预期接收了消息。

框架本身有许多单元和集成测试。有些使用模拟对象,而另一些则使用带有实时 RabbitMQ 代理的集成测试。您可以参考这些测试以获取一些测试场景的思路。

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

@SpringRabbitTest

使用此注解将基础设施 Bean 添加到 Spring 测试 ApplicationContext。在使用例如 @SpringBootTest 时没有必要,因为 Spring Boot 的自动配置会添加这些 Bean。

注册的 Bean 如下

  • CachingConnectionFactory (autoConnectionFactory)。如果存在 @RabbitEnabled,则使用其连接工厂。

  • RabbitTemplate (autoRabbitTemplate)

  • RabbitAdmin (autoRabbitAdmin)

  • RabbitListenerContainerFactory (autoContainerFactory)

此外,还添加了与 @EnableRabbit 关联的 Bean (以支持 @RabbitListener)。

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 {

        ...

	}

}

Mockito Answer<?> 实现

目前有两个 Answer<?> 实现可用于测试。

第一个是 LatchCountDownAndCallRealMethodAnswer,它提供了一个返回 null 并倒计时一个闭锁的 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.getExceptions() 来获取它们的引用。

当与 @RabbitListenerTestRabbitListenerTestHarness 结合使用时,使用 harness.getLambdaAnswerFor("listenerId", true, …​) 来获取监听器的一个正确构建的答案。

@RabbitListenerTestRabbitListenerTestHarness

使用 @RabbitListenerTest 注解您的一个 @Configuration 类会导致框架用一个名为 RabbitListenerTestHarness 的子类替换标准的 RabbitListenerAnnotationBeanPostProcessor(它还通过 @EnableRabbit 启用 @RabbitListener 检测)。

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) {
        ...
    }

}

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

}
1 将 harness 注入到测试用例中,以便我们可以访问 spy。
2 获取 spy 的引用,以便我们可以验证它是否按预期调用。由于这是 sendreceive 操作,因此无需暂停测试线程,因为它已经在 RabbitTemplate 中暂停等待回复。
3 在这种情况下,我们只使用发送操作,所以我们需要一个闭锁来等待容器线程上对监听器的异步调用。我们使用一个 Answer<?> 实现来帮助完成此操作。重要:由于监听器被监视的方式,使用 harness.getLatchAnswerFor() 来为 spy 获取一个正确配置的答案非常重要。
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;
    }

}

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

}
1 将 harness 注入到测试用例中,以便我们可以访问 spy。
2 使用 harness.getNextInvocationDataFor() 来检索调用数据 - 在这种情况下,由于是请求/回复场景,无需等待任何时间,因为测试线程已在 RabbitTemplate 中暂停等待结果。
3 然后我们可以验证参数和结果是否符合预期。
4 这次我们需要一些时间来等待数据,因为它是一个容器线程上的异步操作,我们需要暂停测试线程。
5 当监听器抛出异常时,它可在调用数据的 throwable 属性中获取。
当与 harness 一起使用自定义 Answer<?> 时,为了正常操作,此类答案应继承 ForwardsInvocation 并从 harness (getDelegate("myListener")) 获取实际监听器(而不是 spy),并调用 super.answer(invocation)。请参阅提供的 Mockito Answer<?> 实现 源代码以获取示例。

使用 TestRabbitTemplate

提供了 TestRabbitTemplate,用于执行一些基本的集成测试,而无需代理。当您将其作为 @Bean 添加到测试用例中时,它会发现上下文中所有监听器容器,无论它们是声明为 @Bean 还是 <bean/>,或者使用 @RabbitListener 注解。它目前只支持按队列名称路由。该模板从容器中提取消息监听器并直接在测试线程上调用它。请求-回复消息(sendAndReceive 方法)支持返回回复的监听器。

以下测试用例使用该模板

@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:如果您的测试也需要代理上安装的管理插件,请将其设置为 true

  • purgeAfterEach:(从 2.2 版本开始)当为 true(默认值)时,queues 将在测试之间被清除。

它用于检查代理是否可用,如果不可用则跳过测试。

有时您希望在没有代理的情况下测试失败,例如夜间 CI 构建。要在运行时禁用 BrokerRunningSupport,请将名为 RABBITMQ_SERVER_REQUIRED 的环境变量设置为 true

您可以使用 setter 或环境变量覆盖代理属性,例如主机名。

以下示例展示了如何使用 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/)。

更改主机名会影响 amqpmanagement REST API 连接(除非明确设置了管理 URI)。

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

在您的测试用例中,您可以在创建连接工厂时使用 RabbitAvailableCondition.getBrokerRunning()getConnectionFactory() 返回规则的 RabbitMQ ConnectionFactory。以下示例展示了如何操作

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

此外,RabbitAvailableCondition 支持参数化测试构造函数和方法的参数解析。支持两种参数类型

  • BrokerRunningSupport:实例

  • ConnectionFactoryBrokerRunningSupport 实例的 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() 的静态方法获取条件的连接工厂的引用。

以下测试来自框架,并演示了用法

@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 属性中指定变量名称。

© . This site is unofficial and not affiliated with VMware.