【问题标题】:How to mock AMQP consumers in Camel testing?如何在骆驼测试中模拟 AMQP 消费者?
【发布时间】:2023-03-03 02:19:01
【问题描述】:

假设我有以下路线:

from(rabbitMQUri)
    .to(myCustomerProcessor)
    .choice()
        .when(shouldGotoA)
            .to(fizz)
        .when(shouldGotoB)
            .to(buzz)
        .otherwise()
            .to(foo);

让我们假设myCustomProcessor根据从RabbitMQ消费的消息调整shouldGotoAshouldGotoB

我想对 3 个场景进行单元测试:

  1. 一条“fizz”消息被消费,shouldGotoA 设置为 true,执行第一个 when(...)
  2. 一条“buzz”消息被消费,shouldGotoB 设置为 true,执行第二个 when(...)
  3. 一条“foo”消息被消费,otherwise() 被执行。

我的问题是:我如何模拟/存根 RabbitMQ 端点,以便路由在生产中正常执行,但这样我就不必实际将测试连接到 RabbitMQ 服务器?我需要某种“模拟消息”生产者。

代码示例或 sn-p 会非常有帮助,非常感谢!

【问题讨论】:

    标签: java testing mocking apache-camel


    【解决方案1】:

    使软件(尤其是与外部事物通信的软件)更好地可测试的一个好方法是使用依赖注入。我爱Guice,它是directly supported by camel。 (所有这些东西都会让你学习依赖注入,但我可以向你保证很快就会付出代价)

    在这种情况下,您只需注入“端点”。您可以像这样预先配置端点(将放置在“模块”中)。

    @Provides
    @Named("FileEndpoint")
    private Endpoint fromFileEndpoint() {
        FileEndpoint fileEndpoint = getContext().getEndpoint("file:" + somFolder, FileEndpoint.class);
        fileEndpoint.setMove(".done");
        fileEndpoint.setRecursive(true);
        fileEndpoint.setDoneFileName(FtpRoutes.DONE_FILE_NAME);
        ...
        return fileEndpoint;
    }
    

    您的 RouteBuilder 只需注入端点:

    @Inject
    private MyRoutes(@Named("FileEndpoint") final Endpoint fileEndpoint) {
        this.fileEndpoint = fileEndpoint;
    }
    @Override
    public void configure() throws Exception {
        from(fileEndpoint)....
    }
    

    要轻松测试这样的路由,您需要注入另一个端点进行测试,而不是 FileEndpoint,而是“direct:something”。一个非常简单的方法是"Jukito",它结合了Guice 和Mockito。测试看起来像这样:

    @RunWith(JukitoRunner.class)
    public class OcsFtpTest extends CamelTestSupport {
    
        public static class TestModule extends JukitoModule {
            @Override
            protected void configureTest() {
                bind(CamelContext.class).to(DefaultCamelContext.class).in(TestSingleton.class);
            }
            @Provides
            @Named("FileEndpoint")
            private Endpoint testEndpoint() {
                DirectEndpoint fileEndpoint = getContext().getEndpoint("direct:a", DirectEndpoint.class);
            return fileEndpoint;
            }
        }
        @Inject
        private MyRoutes testObject;
    
       @Test
       ....
    }
    

    现在“testObject”将获得直接端点而不是文件端点。这适用于所有类型的端点,通常适用于所有严重依赖接口的接口/抽象类和 API(骆驼在这里表现出色!)。

    【讨论】:

    • 我在 Module 类中看不到 getContext() 函数。你能帮我解决这个问题吗?
    • getContect() 来自“扩展 CamelTestSupport”。此类提供上下文并启动它(在测试中)。在我的应用程序中,我扩展了 Camel 的 Main 并覆盖了 getContext。这将返回一个注入的上下文。见github.com/dermoritz/camelExamples。它使用 Weld,但上下文是一样的。
    【解决方案2】:

    这是组合合适测试的一种方法。

    首先定义一个空的 Camel Context,其中只有一个 ProducerTemplate:

    <camel:camelContext id="camelContext">
       <camel:template id="producerTemplate" />
    </camel:camelContext>
    

    这样做是为了在执行测试时,我可以控制实际启动哪些路由,因为我不希望所有路由在测试期间启动。

    现在在测试类本身中,您需要对生产者模板和 Camel 上下文的引用。就我而言,我使用的是 Spring,我将它们自动连接到:

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = { "classpath:/spring/spring-test-camel.xml" })
    public class MyTest {
    
        @Autowired
        private ProducerTemplate producerTemplate;
    
        @Autowired
        private CamelContext camelContext;
    

    在测试本身中,将上下文中的 RabbitMQ/ActiveMQ/JMS 组件替换为 seda 或直接组件。例如,用 seda 队列替换所有 JMS 调用。

    camelContext.removeComponent("jms");
    camelContext.addComponent("jms", this.camelContext.getComponent("seda"));
    camelContext.addRoutes(this.documentBatchRouting);
    

    现在,每当您读取或写入 JMS URI 时,它实际上都会进入 seda 队列。这类似于向组件中注入一个新的 URI,但需要较少的配置,并且允许您向路由添加新的端点,而不必担心记住注入所有的 URI。

    最后在测试中,使用生产者模板发送测试消息:

    producerTemplate.sendBody("jms:MyQueue", 2);
    

    然后你的路线应该执行,你可以测试你的期望。

    有两点需要注意:

    1. 您的事务边界可能会发生变化,尤其是当您将 JMS 队列替换为直接组件时

    2. 如果您正在测试多条路线,则必须小心在该路线的测试结束时从 Camel 上下文中删除该路线。

    【讨论】:

    • 感谢@matt helliwell (+1) - 我今晚回家后会检查一下,并会在此处发布反馈!与此同时,一个快速的后续问题:当你说我的“事务边界可能会改变”时,你到底是什么意思?你能举一个具体的例子吗?请记住,我使用 AMQP/RabbitMQ 进行实际消息传递(在生产路线中)。再次感谢!
    • 一种情况是,如果您在正在测试的类中进行数据库更新,并且依赖测试用例来回滚事务。如果您有一个 seda/jms 队列,那么在您的测试用例中创建的事务将不会传播到被测类,因此您无法回滚它们所做的任何数据库更新。如果您使用直接组件,它将正常工作。同样,如果您在路由中将 jms 队列替换为“direct:”组件,那么您将获得通过“direct:”组件传播的任何事务。
    • camelContext.addRoutes(this.documentBatchRouting); 是什么?
    • 我用 rabbitmq 试过这个,组件没有被删除。添加路由后,它会尝试连接到 rabbitmq 而不是 seda。
    • 它似乎只适用于可以接受相同参数的端点。 SEDA 不接受适用于 rabbit 的所有参数,因此在您替换它时会失败。
    【解决方案3】:

    这可能取决于您使用什么组件(AMQP 或 RabbitMQ)进行通信。

    Camel 中示例代码的一个最重要的资源是源代码中的 junit 测试用例。

    这两个文件与您需要的功能相似,但您可能希望在测试用例中四处查看以获取灵感: AMQPRouteTest.java RabbitMQConsumerIntTest.java

    使路由可测试的一种更“基本”的方法是使“来自”uri 成为参数。 假设您将 RouteBuilder 设为如下所示:

       private String fromURI = "amqp:/..";
    
       public void setFromURI(String fromURI){
         this.fromURI = fromURI;
       }
    
       public void configure(){
         from(fromURI).whatever();
       }
    

    然后,您可以在开始单元测试之前在 fromURI 中注入“seda:foobar”端点。 seda 端点是 trivial to test。这假设您不需要测试 AMQP/RabbitMQ 特定的构造,而只需接收有效负载。

    【讨论】:

    • 谢谢@Petter (+1),但我担心有人会给我一个“你应该注入字符串 URI”类型的答案...我需要在 @987654327 中模拟/存根的任何内容@ 实际生成将正确路由的消息。我不相信用seda:foo 注入它会为我做到这一点......有什么想法吗?再次感谢!
    • 嗯.. uri 注入是我的第二个选择(有时它已经足够好了)。查看来自 AMQ 源的两个链接的 .java 文件以获得更合适的模拟
    • 要实际生成消息,只需在测试代码中使用 ProducerTemplate (camel.apache.org/producertemplate.html)。另请记住,如果您将 RibbitMQ 替换为 SEDA 或直接端点,您的事务行为可能会发生变化。
    • 感谢 @matthelliwell (+1) - 但请考虑一下:如果我添加 ProducerTemplate,我将如何注入它而不是 RabbitMQ 端点?
    • 在我的回答中有 AMQP 路由的示例测试类以及来自 Camel 本身的 RabbitMQ 路由。他们确实使用“from”等测试完整的路线。请阅读它们。
    最近更新 更多