Apache Camel的单元测试


几乎大多数软件开发人员都知道Apache Camel是一个事件驱动的框架,具有基于规则的路由和调解引擎,它由Java驱动,许多人都受益于它在与Spring集成方面提供的巨大支持。
但是如何进行单元测试呢?
我们对Camel的测试就像我们对任何java方法进行单元测试一样,通过传递输入和断言响应来进行。Camel测试是一个强大的模块,由其开发者提供,使单元测试的工作与它的使用无缝衔接。
在这篇文章中,我将带你了解3个常见的场景,这些场景几乎在每个使用Camel的应用程序中都是最广泛使用的,并使用Camel的测试模块对它们进行单元测试。完整的代码库可以在这里找到。
 
在此之前,请确保你的项目包含了正确的Camel测试模块依赖,如这里所见。在这篇文章中,我将使用Camel与Junit5和运行在Maven之上的Spring相结合。因此,我的选择是下面这个。

<dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-test-spring-junit5</artifactId>
            <version>${camel.version}</version>
            <scope>test</scope>
</dependency>

 
场景-1
我猜这将是Camel被广泛利用的最流行的用例。使用Camel的文件组件从输入路径中挑选一个文件并进一步处理。在这个特定的案例中,我们从输入路径中挑选一个文件,并观察其格式,如果是CSV,就输出Success,如果是XLSX,就输出Fail,如果是其他文件,就不处理。同样的代码如下。

String fileInbound = UriComponentsBuilder.newInstance().scheme("file")
                .path(Paths.get(inboundPath).toAbsolutePath().toString())
                .queryParam(
"include", "^.*\\.(xlsx|csv)$")
                .build().toString();

        from(fileInbound).id(
"SAMPLE_FILE_ROUTE")
                .log(
"Received file: ${header.CamelFileName}")
                .choice()
                .when((exchange) -> exchange.getIn().getHeader(
"CamelFileName").toString()
                        .endsWith(
"csv")).setHeader("status", simple("SUCCESS")).log("Hurray !! Its a CSV !!")
                .otherwise().setHeader(
"status", simple("FAILURE")).log("Oh No!! Its an XLSX..")
                .end();

单元测试将是相当直接的。模仿上述路由在测试中运行时的行为方式,然后抛出一个文件,观察上述所有场景的正确行为,即CSV的成功,XLSX的失败和txt文件的不拾取。这可能看起来很可怕,但是Camel测试模块提供了一个令人兴奋的方法,在测试时像正常运行一样启动路由,并允许在文件拾取时验证正/负场景,允许将最终响应路由到一个模拟端点。
实现如下:

//Extend the class with CamelTestSupport which Camel Test provides
class SampleFileRouteTest extends CamelTestSupport {
  
 
//Override the createRouteBuilder() and return an object of the actual route class under test
  @Override
    protected RoutesBuilder createRouteBuilder() throws Exception {
        return new SampleFileRoute(
"src/test/resources");
    }
  
 
//Set-Up the mock route to listenForCompletion response once the route is triggered
  @Override
    protected void doPostSetup() throws Exception {
        AdviceWith.adviceWith(context,
"SAMPLE_FILE_ROUTE",
                a -> a.onCompletion().onCompleteOnly().to(
"mock:listenForComplete"));
    }
  
 
//And Finally the test cases for all 3 scenarios, namely Success/Fail and Invalid File
  @Test
    void testForSuccessValidFile() throws IOException, InterruptedException {
        inboundFile = new File(inboundPath.toString(),
"test.csv");
        inboundFile.createNewFile();
        listenForComplete.expectedHeaderReceived(
"status", "SUCCESS");
        listenForComplete.assertIsSatisfied();
    }

    @Test
    void testForFailValidFile() throws IOException, InterruptedException {
        inboundFile = new File(inboundPath.toString(),
"test.xlsx");
        inboundFile.createNewFile();
        listenForComplete.expectedHeaderReceived(
"status", "FAILURE");
        listenForComplete.assertIsSatisfied();
    }

    @Test
    void testForInvalidFile() throws IOException, InterruptedException {
        inboundFile = new File(inboundPath.toString(),
"test.txt");
        inboundFile.createNewFile();
        listenForComplete.expectedHeaderReceived(
"status","SUCCESS");
        listenForComplete.assertIsNotSatisfied();
    }
  
}
 

 
场景-2
另一个有趣的用例是FTP,我广泛地看到人们在使用Camel。Camel提供了一个叫FTP的组件,它以简单和非常干净的方式为所有的FTP传输提供了开箱即用的支持。我们在这方面的用例将是相当直接的。从入站路径中选取一个文件并将其SFTP到远程服务器的一个路径。成功时打印出一个日志,表明传输是通过的,失败时打印同样的日志。下面是这个逻辑的代码。

//Prepare the respective inbound and outbound URLS
String fileInbound = UriComponentsBuilder.newInstance().scheme(
"file")
        .path(inboundPath)
        .build().toString();

String outboundLocation = UriComponentsBuilder.newInstance()
        .scheme(
"sftp").host(sftpInfo.getHost()).port(sftpInfo.getPort()).userInfo(sftpInfo.getUser())
        .path(sftpInfo.getPath()).queryParam(
"preferredAuthentications", "password")
        .queryParam(
"password", sftpInfo.getPassword())
        .queryParam(
"jschLoggingLevel", "WARN")
        .build().toString();


//Use Camel-FTP Support and effect the transfer.. namely pick from the inbound and send to output
//and observe success/failures
from(fileInbound).id(
"SAMPLE_SFTP_ROUTE")
        .log(
"Starting SFTP Transfer for File: ${header.CamelFileName}")
        .to(outboundLocation)
        .log(
"SFTP Transfer Success for File: ${header.CamelFileName}");

onException(Exception.class)
        .maximumRedeliveries(2)
        .redeliveryDelay(20)
        .log(
"SFTP Transfer Failure for File: ${header.CamelFileName}")
        .handled(true);

测试案例也很简单,就是观察连接到SFTP服务器的正常启动路线,以及正向流量的正常传输,而对于负向流量,路线无法启动,网络断开,意味着传输没有发生。此刻你可能会想到我们将在这里使用的SFTP服务器的下落。对于那些听说过fake-sftp-server-lambda这个内存SFTP解决方案的人来说,这并不奇怪。对于那些第一次听说的人来说,fake-sftp-server是一个MIT许可的项目,它写在Apache SSHD项目的SFTP服务器之上,以方便在测试执行中使用内存SFTP服务器。利用Camel和Fake-Sftp-Server的力量,对同样的测试进行了如下操作。

//Extend the test class with Test Support Camel provides
class SampleSftpRouteTest extends CamelTestSupport {
  
   
//Start the in-memory SFTP Server using Fake-Sftp-Server-Lambda and make it listen
    @Override
    @BeforeEach
    public void setUp() throws Exception {
        withSftpServer(server -> {
            server.addUser(
"tst", "tst");
            sftpInfo = SftpInfo.builder().host(
"0.0.0.0").port(server.getPort()).path("/").build();
        });
        super.setUp();
    }
  
    
   
//Test Both the Success and Fail Scenarios.
   
//For Success - Test if the file is transfered successful upon created in I/P Path
   
//For Failure - Test if the route throws exception upon start and the file transfer does'nt happen
    @Test
    void testsftpSuccess() throws Exception {
        inboundFile = new File(inboundPath.toString(),
"test.txt");
        inboundFile.createNewFile();
        await().atMost(10, TimeUnit.SECONDS);
        withSftpServer(server -> assertTrue(server.existsFile(
"/test.txt")));
        assertFalse(inboundFile.exists());
    }

    @Test
    void testsftpFailure() throws Exception {
        sftpInfo.setUser(
"user");
        sftpInfo.setPassword(
"wrong");
        inboundFile = new File(inboundPath.toString(),
"test.txt");
        inboundFile.createNewFile();
        await().atMost(10, TimeUnit.SECONDS);
        withSftpServer(server -> assertFalse(server.existsFile(
"/test.txt")));
        assertTrue(inboundFile.exists());
    }

}
 

 
场景-3
这无疑是最有趣的方面,对于大多数开发者来说,使用Camel是为了它提供的多跳支持。当生产者发送消息交换时,Camel Direct提供了对任何消费者的直接、同步的调用。这个端点可用于连接同一Camel上下文中的现有路由。在我们的用例中,我们实现了所提供的设施,并以同步的方式在各个逻辑块中进行路由。这些代码或多或少都是不言自明的。不过,如果有不清楚的地方,请随时查看评论。

//Build the route as below. It all starts from India and proceeds to Delhi, Mumbai, Kolkata and Chennai
//In each and every stop, it goes into individual logics and has it executed.
//The block of individual logic for Delhi is also given below.
  from(
"direct:India").id("SAMPLE_MULTIHOP_ROUTE")
                .log(
"Welcome to India !!")
                .to(
"direct:Delhi")
                .to(
"direct:Mumbai")
                .to(
"direct:Kolkata")
                .to(
"direct:Chennai");

  from(
"direct:Delhi")
          .bean(Delhi.class);

  from(
"direct:Mumbai")
          .bean(Mumbai.class);

  from(
"direct:Kolkata")
          .bean(Kolkata.class);

  from(
"direct:Chennai")
          .bean(Chennai.class);

//Execution Logic inside Delhi.class
@Component
public class Delhi {

    @Handler
    public void handler(@RequestBody String name) {
        System.out.println(
"Hello Mr " + name + "!!! Welcome to Delhi !!");
    }
}

测试这一点非常简单,使用Camel的Producer-Template明确地触发起始点,然后模拟所有其他停止点的豆子,并验证与模拟对象的交互。注意,重要的是在Camel测试上下文中注入被模拟的对象,以观察和断言其行为。同样的测试将如下所示。

@ExtendWith(MockitoExtension.class)
class SampleMultiHopRouteTest extends CamelTestSupport {

    @InjectMocks
    private SampleMultiHopRoute sampleMultiHopRoute;

    @Mock
    private Delhi delhi;

    @Mock
    private Mumbai mumbai;

    @Mock
    private Kolkata kolkata;

    @Mock
    private Chennai chennai;

    @Override
    protected RoutesBuilder createRouteBuilder() throws Exception {
        Registry registry = context.getRegistry();
        registry.bind("delhi", Delhi.class, delhi);
        registry.bind(
"mumbai", Mumbai.class, mumbai);
        registry.bind(
"kolkata", Kolkata.class, kolkata);
        registry.bind(
"chennai", Chennai.class, chennai);
        return sampleMultiHopRoute;
    }

    @Test
    void testMultiHopRoute() {
        doNothing().when(delhi).handler(
"Junit");
        doNothing().when(mumbai).handler(
"Junit");
        doNothing().when(kolkata).handler(
"Junit");
        doNothing().when(chennai).handler(
"Junit");
        template.sendBody(
"direct:India", "Junit");
        verify(delhi).handler(
"Junit");
        verify(mumbai).handler(
"Junit");
        verify(kolkata).handler(
"Junit");
        verify(chennai).handler(
"Junit");
    }
}

还有许多其他惊人的用例,Camel实际上被用于这些用例。当然,所有这些都有非常令人兴奋的方法来进行单元测试,Camel-Test在每一个单元测试中都发挥了巨大的作用,从提供一个正确的环境来实时进行单元测试到断言这些测试。是的!有一些调整是必须做的,以模拟用例的实际工作方式,使其得到全面的测试。但无论如何,Camel-测试支持是其开发团队提供的一个奇妙的功能,使其测试强大而简单。