Apache Camel的單元測試

banq發表於2022-02-10

幾乎大多數軟體開發人員都知道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-測試支援是其開發團隊提供的一個奇妙的功能,使其測試強大而簡單。

相關文章